Skip to main content

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:

FamilyHow it generatesAPI
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.

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.

The generators that ship

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).

Preview overwrites the displayed capture

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.

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()

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.

Demo output is deterministic here

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.

The acquisition waits for the first chunk

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).

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.

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.