Stimulus and capture together
A device that generates drives its outputs while it captures, so the instrument stimulates the board as well as watching it. Send a known frame, capture the answer, decode both, all in one workspace.
Two device families generate, in two different ways:
| Family | How it generates | API |
|---|---|---|
| SP1000G (SP1018G, SP1036G, SP1054G) | A pattern generator script runs on the server and feeds the device's generator while the capture runs. | workspace.pattern |
| SQ (SQ25, SQ50, SQ100, SQ200) | Each channel has a generation row, filled by a builder script or a constant level. The capture replays the rows of the channels set as outputs. | workspace.generation, capture.run(operation=...) |
The first sections cover the SP1000G pattern generator. The SQ family has its own section at the end.
The pattern generator is a JavaScript file in the server's script library, one
implementing on_pattern_generate. Writing one is covered by the
pattern generator scripting docs;
this page is about driving it from outside. The examples use sp1018, the
SP1018G demo device; its channels are group-prefixed (G1 PWM,
G1 I2C SCL). preview() runs on any device because it is interpreted by the
server. See Pattern generator.
Preview first, hardware second​
preview() runs the generator script through an interpreter instead of the
device, and the result replaces the workspace data as an acquisition would. The
script runs unchanged, so the preview shows the real program. Develop against
the preview with no hardware attached, then run the same configuration for real.
- Python
- NodeJS
- Rust
- C
from ikalogic_scanastudio import ScanaStudio
GENERATOR = "pwm.js"
with ScanaStudio.connect() as server:
workspace = server.create("sp1018")
# What does this generator take? Ask it.
for option in workspace.pattern.options(GENERATOR):
print(option)
workspace.pattern.add(GENERATOR, {
"channel": workspace.channel("G1 PWM"),
"simple_freq_val": 1_000_000, # 1 MHz
"simple_pwm_val": 25, # 25 % duty
"nb_of_cycles": "100",
})
print(workspace.pattern.current)
# How long the pattern lasts, counted without generating anything.
print(workspace.pattern.duration())
# Interpret it, without touching any hardware.
workspace.pattern.preview(timeout=120.0)
# The preview landed in the workspace exactly as a capture would.
rate = workspace.capture.sample_rate
edges = list(workspace.data.transitions(channel=workspace.channel("G1 PWM")))
print(f"preview: {workspace.capture.last_sample} samples "
f"at {rate} S/s, {len(edges)} edges")
workspace.close()
Example output. On the sp1018 demo device:
channel: Target channel (ch_selector) = None
simple_freq_val [Fixed duty cycle]: Frequency (engineering_input) in Hz = 6562550.0
simple_pwm_val [Fixed duty cycle]: Duty cycle (engineering_input) in % = 50.0
mod_type [Modulated frequency]: Modulation type (combo) one of ['Sine', 'Triangle', 'SawTooth'] = 'Sine'
...
nb_of_cycles: Number of cycles (0 = infinite loop) (text_input) = '1000'
instance_id=1 file_name='pwm.js' name='PWM Builder on CH 8' enabled=True pausable=False paused=False
{'seconds': 0.0001, 'endless': False, 'over_limit': False}
preview: 100000 samples at 1000000000 S/s, 201 edges
Those 201 edges are the program you asked for: the initial level, then 100 cycles of two edges each. Measuring the preview gives 1 MHz at 25 % duty, and the 100 µs the pattern lasts match the 100 000 samples at 1 GS/s.
import { ScanaStudio } from '@ikalogic/scanastudio';
const GENERATOR = 'pwm.js';
const server = await ScanaStudio.connect();
try {
const workspace = await server.create('sp1018');
// What does this generator take? Ask it.
for (const option of await workspace.pattern.options(GENERATOR)) {
console.log(option);
}
await workspace.pattern.add(GENERATOR, {
channel: workspace.channel('G1 PWM'),
simple_freq_val: 1_000_000,
simple_pwm_val: 25,
nb_of_cycles: '100',
});
console.log(workspace.pattern.current);
// How long the pattern lasts, counted without generating anything.
console.log(await workspace.pattern.duration());
// Interpret it, without touching any hardware.
await workspace.pattern.preview(120_000);
// The preview landed in the workspace exactly as a capture would.
const edges = [];
for await (const edge of workspace.data.transitions(workspace.channel('G1 PWM'))) {
edges.push(edge);
}
console.log(`preview: ${workspace.capture.last_sample} samples, ${edges.length} edges`);
workspace.close();
} finally {
server.close();
}
use std::time::Duration;
use scanastudio_client::{values, ScanaStudio, DEFAULT_URL, Result};
const GENERATOR: &str = "pwm.js";
#[tokio::main]
async fn main() -> Result<()> {
let server = ScanaStudio::connect(DEFAULT_URL).await?;
let workspace = server.create("sp1018").await?;
// What does this generator take? Ask it.
for option in workspace.pattern().options(GENERATOR).await? {
println!("{option}");
}
let pwm = workspace.channel("G1 PWM")?;
workspace.pattern().add(GENERATOR, &values([
("channel", pwm.into()),
("simple_freq_val", 1_000_000i64.into()),
("simple_pwm_val", 25i64.into()),
("nb_of_cycles", "100".into()),
])).await?;
// How long the pattern lasts, counted without generating anything.
let duration = workspace.pattern().duration(None).await?;
println!("{} s, endless: {}", duration.seconds, duration.endless);
// Interpret it, without touching any hardware.
workspace.pattern().preview(Duration::from_secs(120)).await?;
// The preview landed in the workspace exactly as a capture would.
let edges = workspace.data().transitions(pwm, 0, None).await?;
println!("preview: {} samples, {} edges",
workspace.capture().last_sample(), edges.len());
workspace.close()?;
Ok(())
}
#include <stdio.h>
#include "scanastudio.h"
#define GENERATOR "pwm.js"
int main(void) {
ss_server *server = ss_server_connect(NULL);
ss_workspace *workspace = ss_server_create(server, "sp1018");
/* What does this generator take? Ask it. */
char *options = ss_pattern_options(workspace, GENERATOR);
if (options) { printf("%s\n", options); ss_string_free(options); }
char values[192];
snprintf(values, sizeof values,
"{\"channel\": %d, \"simple_freq_val\": 1000000,"
" \"simple_pwm_val\": 25, \"nb_of_cycles\": \"100\"}",
ss_workspace_channel(workspace, "G1 PWM"));
char *generator = ss_pattern_add(workspace, GENERATOR, values);
if (generator) { printf("%s\n", generator); ss_string_free(generator); }
/* How long the pattern lasts, as JSON. A negative limit keeps the default. */
char *duration = ss_pattern_duration(workspace, -1.0);
if (duration) { printf("%s\n", duration); ss_string_free(duration); }
/* Interpret it, without touching any hardware. */
ss_pattern_preview(workspace, 120.0);
/* The preview landed in the workspace exactly as a capture would. */
ss_edge edges[4096];
ptrdiff_t count = ss_data_transitions(workspace, ss_workspace_channel(workspace, "G1 PWM"),
0, -1, edges, 4096);
printf("preview: %lld samples at %llu S/s, %ld edges\n",
(long long)ss_capture_last_sample(workspace),
(unsigned long long)ss_capture_sample_rate(workspace), (long)count);
ss_workspace_close(workspace);
ss_workspace_free(workspace);
ss_server_disconnect(server);
return 0;
}
A stock library carries pwm.js, frequency_modulation.js, i2c_scanner.js,
rgb_led.js, csv_import.js and sp1000-autotest.js. List what your server
has with server.scripts.generators() after creating a workspace (see
the note on listing scripts).
The preview replaces whatever the workspace held. Save anything you still need first.
Stimulate and capture, for real​
On hardware, the generator runs alongside the acquisition.
set_generate_on_trigger decides when the pattern plays.
- Python
- NodeJS
- Rust
- C
from ikalogic_scanastudio import ScanaStudio, Trigger
with ScanaStudio.connect() as server:
# The SP1018G's key, as server.devices() lists it.
workspace = server.create("hw:1004250000604")
workspace.pattern.add("i2c_scanner.js", {
"ch_scl": workspace.channel("G1 I2C SCL"),
"ch_sda": workspace.channel("G1 I2C SDA"),
})
workspace.pattern.set_enabled(True)
# Play the pattern when the acquisition triggers, not when it starts.
workspace.pattern.set_generate_on_trigger(True)
# Arm on SDA going low: the start condition the scanner will drive.
workspace.capture.run(
samples=2_000_000,
sample_rate=25_000_000,
trigger=Trigger.falling(channel=workspace.channel("G1 I2C SDA"), position=0.05),
timeout=60.0,
)
# Decode what actually went out, and what answered.
i2c = workspace.decoders.add("i2c.js", {
"ch_scl": workspace.channel("G1 I2C SCL"),
"ch_sda": workspace.channel("G1 I2C SDA"),
}, wait=True)
packets = list(i2c.packets())
probed = [p for p in packets if p.title == "Address"]
unanswered = sum(1 for p in packets if p.title == "Addr Nack")
print(f"probed {len(probed)} addresses, {unanswered} went unanswered")
for packet in probed[:3]:
print(" ", packet.content)
# Generation outlives the capture: release the outputs.
workspace.pattern.stop_generation()
workspace.close()
import { ScanaStudio, Trigger } from '@ikalogic/scanastudio';
const server = await ScanaStudio.connect();
try {
// The SP1018G's key, as server.devices() lists it.
const workspace = await server.create('hw:1004250000604');
await workspace.pattern.add('i2c_scanner.js', {
ch_scl: workspace.channel('G1 I2C SCL'),
ch_sda: workspace.channel('G1 I2C SDA'),
});
workspace.pattern.set_enabled(true);
// Play the pattern when the acquisition triggers, not when it starts.
workspace.pattern.set_generate_on_trigger(true);
// Arm on SDA going low: the start condition the scanner will drive.
await workspace.capture.run({
samples: 2_000_000,
sample_rate: 25_000_000,
trigger: Trigger.falling(workspace.channel('G1 I2C SDA'), { position: 0.05 }),
timeout_ms: 60_000,
});
// Decode what actually went out, and what answered.
const i2c = await workspace.decoders.add(
'i2c.js',
{ ch_scl: workspace.channel('G1 I2C SCL'), ch_sda: workspace.channel('G1 I2C SDA') },
{ wait: true },
);
const packets = [];
for await (const packet of i2c.packets()) packets.push(packet);
const probed = packets.filter((p) => p.title === 'Address');
const unanswered = packets.filter((p) => p.title === 'Addr Nack').length;
console.log(`probed ${probed.length} addresses, ${unanswered} went unanswered`);
for (const packet of probed.slice(0, 3)) console.log(' ', packet.content);
// Generation outlives the capture: release the outputs.
workspace.pattern.stop_generation();
workspace.close();
} finally {
server.close();
}
use std::time::Duration;
use scanastudio_client::{
proto::RowFilter, values, CaptureRequest, ScanaStudio, Trigger, TriggerExt,
DEFAULT_URL, Result,
};
#[tokio::main]
async fn main() -> Result<()> {
let server = ScanaStudio::connect(DEFAULT_URL).await?;
// The SP1018G's key, as server.devices() lists it.
let workspace = server.create("hw:1004250000604").await?;
let scl = workspace.channel("G1 I2C SCL")?;
let sda = workspace.channel("G1 I2C SDA")?;
workspace.pattern().add("i2c_scanner.js", &values([
("ch_scl", scl.into()),
("ch_sda", sda.into()),
])).await?;
workspace.pattern().set_enabled(true)?;
// Play the pattern when the acquisition triggers, not when it starts.
workspace.pattern().set_generate_on_trigger(true)?;
// Arm on SDA going low: the start condition the scanner will drive.
workspace.capture().run(
&CaptureRequest::new(2_000_000, 25_000_000)
.trigger(Trigger::falling(sda).at(0.05)),
Duration::from_secs(60),
).await?;
// Decode what actually went out, and what answered.
let i2c = workspace.decoders()
.add("i2c.js", &values([("ch_scl", scl.into()), ("ch_sda", sda.into())]))
.await?;
workspace.decoders().wait_for_instances(&[i2c.instance_id], Duration::from_secs(300)).await?;
let filter = RowFilter { sources: vec![i2c.instance_id], ..Default::default() };
let packets = workspace.data().packets(&filter).await?;
let probed: Vec<&str> = packets
.iter()
.filter(|p| p.title == "Address")
.map(|p| p.content.as_str())
.collect();
let unanswered = packets.iter().filter(|p| p.title == "Addr Nack").count();
println!("probed {} addresses, {unanswered} went unanswered", probed.len());
for content in probed.iter().take(3) {
println!(" {content}");
}
// Generation outlives the capture: release the outputs.
workspace.pattern().stop_generation()?;
workspace.close()?;
Ok(())
}
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "scanastudio.h"
int main(void) {
ss_server *server = ss_server_connect(NULL);
if (!server) return 1;
/* The SP1018G's key, as ss_server_devices() lists it. */
ss_workspace *workspace = ss_server_create(server, "hw:1004250000604");
if (!workspace) { fprintf(stderr, "%s\n", ss_last_error()); return 1; }
int scl = ss_workspace_channel(workspace, "G1 I2C SCL");
int sda = ss_workspace_channel(workspace, "G1 I2C SDA");
char values[64];
snprintf(values, sizeof values, "{\"ch_scl\": %d, \"ch_sda\": %d}", scl, sda);
char *generator = ss_pattern_add(workspace, "i2c_scanner.js", values);
if (!generator) { fprintf(stderr, "pattern: %s\n", ss_last_error()); return 1; }
ss_string_free(generator);
ss_pattern_set_enabled(workspace, true);
/* Play the pattern when the acquisition triggers, not when it starts. */
ss_pattern_set_generate_on_trigger(workspace, true);
/* Arm on SDA going low: the start condition the scanner will drive. */
char *trigger = ss_trigger_edge((uint16_t)sda, "falling", 0.05, NULL);
char request[192];
snprintf(request, sizeof request,
"{\"samples\": 2000000, \"sample_rate\": 25000000, \"trigger\": %s}",
trigger);
ss_string_free(trigger);
if (ss_capture_run(workspace, request, 60.0) < 0) {
fprintf(stderr, "capture: %s\n", ss_last_error());
return 1;
}
/* Decode what actually went out, and what answered. */
uint32_t i2c = ss_decoder_add(workspace, "i2c.js", values);
if (!i2c || ss_decoder_wait(workspace, i2c, 300.0) != ss_status_Ok) return 1;
char filter[64];
snprintf(filter, sizeof filter, "{\"sources\": [%u]}", i2c);
ss_packets *batch = ss_packets_read(workspace, filter);
if (!batch) { fprintf(stderr, "packets: %s\n", ss_last_error()); return 1; }
size_t count = ss_packets_count(batch);
ss_packet *packets = malloc(sizeof *packets * count);
ss_packets_copy(batch, 0, packets, count);
size_t probed = 0, unanswered = 0;
for (size_t i = 0; i < count; i += 1) {
if (strcmp(packets[i].title, "Address") == 0) probed += 1;
if (strcmp(packets[i].title, "Addr Nack") == 0) unanswered += 1;
}
printf("probed %zu addresses, %zu went unanswered\n", probed, unanswered);
free(packets);
ss_packets_free(batch);
/* Generation outlives the capture: release the outputs. */
ss_pattern_stop(workspace);
ss_workspace_close(workspace);
ss_workspace_free(workspace);
ss_server_disconnect(server);
return 0;
}
Example output. On the sp1018 demo device, with preview() in place of a
real acquisition:
preview: 33861632 samples at 1000000000 S/s
probed 112 addresses, 112 went unanswered
Write to 0x08
Write to 0x09
Write to 0x0a
112 is the whole 7-bit address space minus the reserved ranges, which is what
skip_addresses asks for. Every probe goes unanswered because nothing is on the
far end of a demo device. On a real bus, the addresses that acknowledge come
back without a matching Addr Nack, and those are your devices.
Elsewhere a demo device generates pseudo-random signal, so a decoder finds
structure by chance. Here the stimulus is a program you wrote, so the decoded
result is real protocol traffic: START, Address, Addr Nack, STOP, 112
times over. This is the one setup where you can assert on exact packet content
with no hardware attached.
Notes for your own bench​
One generator per workspace. The device has a single pattern generator, so
a workspace has one too. pattern.add() replaces whatever was there and returns
the new pattern.current, or None (null in NodeJS) if the server has not
reported it yet. There is no list of generators.
Generation outlives the capture. The outputs keep playing after the
acquisition has ended. They stop at the end of the pattern, at the next capture
start, or when you call pattern.stop_generation() (ss_pattern_stop in C).
Call it at the end of a test so the board is not left driven.
Size the capture from the pattern. pattern.duration() counts how long the
pattern lasts, loops included, without generating anything. It returns
seconds, endless (the pattern never ends by itself) and over_limit
(counting stopped at the limit_seconds you passed). Multiply seconds by the
sample rate to get the samples that cover the whole stimulus. The I²C scanner
above lasts 0.034 s, which is the 33 861 632 samples of its preview at 1 GS/s.
Read the script's output when it fails. preview() raises ScriptError
when the script fails, and its logs hold what the script printed. That is
usually where the reason is.
A paused pattern needs resume(). A generator script can stop between two
chunks with builder_add_pause. pattern.resume() (ss_pattern_resume in C)
lets it continue with the next chunk.
Decide when the pattern plays. set_generate_on_trigger(True) starts the
stimulus on the acquisition's trigger. False starts it as soon as the
acquisition does. For a request/response test the first is usually what you
want: arm on something, then speak.
The generator configures its own pins. Pin directions, pulls, voltages and
idle states for the generated channels come from the generator. Do not set the
same pins in capture.start().
Develop on the preview. It needs no board and runs the real script. Get the program right there, then change the device key.
On real hardware the capture is only armed once the first chunk of the pattern
has been loaded and started, so that the stimulus cannot arrive after the
trigger. If the script generates nothing at all, or refuses the configuration
it was given, the acquisition is refused with a Refused error that carries the
server's reason.
Generation rows on the SQ family​
An SQ25, SQ50, SQ100 or SQ200 has no pattern generator script. Each channel has
a generation row instead: a waveform kept beside the capture, which the
next generation replays on the channels set as outputs. A row is filled by a
builder script, by generation.fill() with a copy of the last capture or a
constant level, or by drawing it in the ScanaStudio interface.
capture.run() takes operation: capture (the default), generation to
replay the rows without capturing, or mixed to replay and capture at the same
time. channel_io sets each channel to input or output, and
generator_samples says how many samples of the rows to replay
(generator_loop=True replays them until the capture is stopped).
- Python
- NodeJS
- Rust
- C
from ikalogic_scanastudio import ScanaStudio
with ScanaStudio.connect() as server:
workspace = server.create("sq25")
tx = workspace.channel("UART TX")
# The rows are replayed at this length and rate.
workspace.generation.set_layout(samples=100_000, sample_rate=25_000_000)
# A builder script writes the row: 100 kHz at 25 % duty on UART TX.
builder = workspace.generation.add_builder("pwm.js", {
"channel": tx,
"simple_freq_val": 100_000,
"simple_pwm_val": 25,
})
print(builder)
# Drive UART TX from its row and capture every channel at the same time.
last = workspace.capture.run(
operation="mixed",
samples=100_000,
sample_rate=25_000_000,
generator_samples=100_000,
channel_io=["output", "input", "input", "input"],
timeout=60.0,
)
# The same measurement on the row that was sent and on what was captured.
kinds = ["freq_avg", "duty_avg"]
workspace.measures.between(0, last, channel=tx, generator=True, kinds=kinds)
workspace.measures.between(0, last, channel=tx, kinds=kinds)
for measure in workspace.measures.wait():
print(", ".join(f"{r.kind} {r.value:.6g}" for r in measure.results))
workspace.close()
Example output. On the sq25 demo device:
instance_id=1 file_name='pwm.js' name='PWM Builder on CH 1' channels=[0] status='built' error=None
freq_avg 100000, duty_avg 0.248467
freq_avg 100000, duty_avg 0.248467
The first line measures the row that was sent, the second what was captured on
the same channel. generator=True points a measurement, transitions() or
level_at() at the row instead of the capture.
import { ScanaStudio } from '@ikalogic/scanastudio';
const server = await ScanaStudio.connect();
try {
const workspace = await server.create('sq25');
const tx = workspace.channel('UART TX');
// The rows are replayed at this length and rate.
await workspace.generation.set_layout(100_000, 25_000_000);
// A builder script writes the row: 100 kHz at 25 % duty on UART TX.
const builder = await workspace.generation.add_builder('pwm.js', {
channel: tx,
simple_freq_val: 100_000,
simple_pwm_val: 25,
});
console.log(builder);
// Drive UART TX from its row and capture every channel at the same time.
const last = await workspace.capture.run({
operation: 'mixed',
samples: 100_000,
sample_rate: 25_000_000,
generator_samples: 100_000,
channel_io: ['output', 'input', 'input', 'input'],
timeout_ms: 60_000,
});
// The same measurement on the row that was sent and on what was captured.
const kinds = ['freq_avg', 'duty_avg'];
await workspace.measures.between(0, last, { channel: tx, generator: true, kinds });
await workspace.measures.between(0, last, { channel: tx, kinds });
for (const measure of await workspace.measures.wait()) {
console.log(measure.results.map((r) => `${r.kind} ${r.value.toPrecision(6)}`).join(', '));
}
workspace.close();
} finally {
server.close();
}
The Rust SDK can prepare the rows (workspace.generation(): set_layout,
fill, add_builder), but its CaptureRequest has no operation,
generator_samples, generator_loop or channel_io yet, so it cannot start a
generation or a mixed capture. Use the Python or NodeJS SDK for this step.
The C library can prepare the rows (ss_generation_set_layout,
ss_generation_fill, ss_builder_add), but its capture request does not take
operation, generator_samples, generator_loop or channel_io yet, so it
cannot start a generation or a mixed capture. Use the Python or NodeJS SDK for
this step.
generation.set_layout() sets how long the rows are and the rate they are timed
against. A row longer than what is replayed is truncated, and a shorter one
holds its last level. A builder writes the rows of the channels its
configuration names; update_builder() and rebuild_builder() run it again,
and remove_builder() detaches it while the rows keep their content. See
Workspace and devices
for the full list.