Stimulus and capture together
The pattern generator drives the device's outputs from a script, so the instrument stimulates the board as well as watching it. Send a known frame, capture the answer, decode both, all in one workspace.
The 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.
Pattern generation is an SP1000G-series feature (SP1018G, SP1036G, SP1054G).
The examples on this page 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)
# 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 = nan
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
preview: 6000 samples at 1000000000 S/s, 13 edges
Those 13 edges are the program you asked for: measuring the first full cycle gives 1.000 MHz at 24.8 % duty, against the 1 MHz and 25 % requested.
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);
// 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?;
// 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); }
/* 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:
workspace = server.create("hw:SP1018G-000123")
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)
workspace.close()
import { ScanaStudio, Trigger } from '@ikalogic/scanastudio';
const server = await ScanaStudio.connect();
try {
const workspace = await server.create('hw:SP1018G-000123');
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);
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?;
let workspace = server.create("hw:SP1018G-000123").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}");
}
workspace.close()?;
Ok(())
}
See above.
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; there is
no list and no instance id, only pattern.current.
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, the acquisition is refused with
a Refused error.