Trigger on a protocol frame
Start a capture on one I²C address, or on one byte or string going by on a UART line. The decoder that reads the bus also builds the trigger, and the device runs it in hardware, so the capture starts on that exact frame however busy the bus is.
The program
- Python
- NodeJS
- Rust
- C
import sys
from ikalogic_scanastudio import ScanaStudio
SCRIPT = "i2c.js"
def main(device: str = "", address: str = "0x41") -> int:
with ScanaStudio.connect() as server:
if not device:
hardware = [d for d in server.devices() if not d.demo]
device = hardware[0].key if hardware else "sp259"
workspace = server.create(device)
# SCL on CH1, SDA on CH2
i2c = workspace.decoders.add(SCRIPT, {"ch_scl": 0, "ch_sda": 1})
# What the trigger takes, as the script describes it.
for option in i2c.trigger_options():
print(" ", option)
# trig_addr alone selects the "on I2C address" alternative.
trigger = i2c.trigger({"trig_addr": address}, position=0.5)
workspace.capture.run(samples=2_000_000, sample_rate=25_000_000, trigger=trigger)
i2c.wait()
# The T marker is where the trigger fired.
t = next(m.sample for m in workspace.markers.all() if m.trigger)
print(f"triggered at {workspace.capture.seconds(t) * 1e3:.3f} ms")
window = workspace.capture.sample_rate // 1000 # 1 ms either side
for item in i2c.items():
if abs(item.start - t) < window:
at = workspace.capture.seconds(item.start) * 1e3
print(f" {at:9.3f} ms {item.content}")
workspace.close()
return 0
if __name__ == "__main__":
raise SystemExit(main(*sys.argv[1:]))
import { ScanaStudio, describe_option } from '@ikalogic/scanastudio';
const SCRIPT = 'i2c.js';
async function main(device = '', address = '0x41') {
const server = await ScanaStudio.connect();
try {
if (!device) {
const hardware = (await server.devices()).filter((d) => !d.demo);
device = hardware[0]?.key ?? 'sp259';
}
const workspace = await server.create(device);
// SCL on CH1, SDA on CH2
const i2c = await workspace.decoders.add(SCRIPT, { ch_scl: 0, ch_sda: 1 });
// What the trigger takes, as the script describes it.
for (const option of await i2c.trigger_options()) console.log(' ', describe_option(option));
// trig_addr alone selects the "on I2C address" alternative.
const trigger = await i2c.trigger({ trig_addr: address }, { position: 0.5 });
await workspace.capture.run({ samples: 2_000_000, sample_rate: 25_000_000, trigger });
await i2c.wait();
// The T marker is where the trigger fired.
const t = workspace.markers.all().find((m) => m.trigger).sample;
console.log(`triggered at ${(workspace.capture.seconds(t) * 1e3).toFixed(3)} ms`);
const window = workspace.capture.sample_rate / 1000; // 1 ms either side
for await (const item of i2c.items()) {
if (Math.abs(item.start - t) < window) {
const at = (workspace.capture.seconds(item.start) * 1e3).toFixed(3).padStart(9);
console.log(` ${at} ms ${item.content}`);
}
}
workspace.close();
return 0;
} finally {
server.close();
}
}
process.exit(await main(...process.argv.slice(2)));
use std::time::Duration;
use scanastudio_client::{
proto::RowFilter, values, CaptureRequest, ScanaStudio, TriggerExt, DEFAULT_URL, Result,
};
#[tokio::main]
async fn main() -> Result<()> {
let server = ScanaStudio::connect(DEFAULT_URL).await?;
let devices = server.devices().await?;
let device = devices.iter().find(|d| !d.demo).map_or("sp259", |d| d.key.as_str());
let workspace = server.create(device).await?;
// SCL on CH1, SDA on CH2
let i2c = workspace.decoders()
.add("i2c.js", &values([("ch_scl", 0.into()), ("ch_sda", 1.into())]))
.await?;
// What the trigger takes, as the script describes it.
for option in workspace.decoders().trigger_options(i2c.instance_id).await? {
println!(" {option}");
}
// trig_addr alone selects the "on I2C address" alternative.
let trigger = workspace.decoders()
.trigger(i2c.instance_id, &values([("trig_addr", "0x41".into())]))
.await?
.at(0.5);
workspace.capture().run(
&CaptureRequest::new(2_000_000, 25_000_000).trigger(trigger),
Duration::from_secs(300),
).await?;
workspace.decoders().wait_for_instances(&[i2c.instance_id], Duration::from_secs(300)).await?;
// The T marker is where the trigger fired.
let t = workspace.markers().all().into_iter()
.find(|m| m.trigger).map_or(0, |m| m.sample);
println!("triggered at {:.3} ms", workspace.capture().seconds(t) * 1e3);
let filter = RowFilter { sources: vec![i2c.instance_id], ..Default::default() };
for item in workspace.data().items(&filter, &[]).await? {
if (item.start - t).abs() < 25_000 {
println!(" {:9.3} ms {}", workspace.capture().seconds(item.start) * 1e3, item.content);
}
}
workspace.close()?;
Ok(())
}
#include <stdio.h>
#include <stdlib.h>
#include "scanastudio.h"
int main(int argc, char **argv) {
const char *device = argc > 1 ? argv[1] : "sp259"; /* "hw:<serial>" for the real thing */
ss_server *server = ss_server_connect(NULL);
if (!server) return 1;
ss_workspace *workspace = ss_server_create(server, device);
if (!workspace) return 1;
/* SCL on CH1, SDA on CH2 */
uint32_t i2c = ss_decoder_add(workspace, "i2c.js", "{\"ch_scl\": 0, \"ch_sda\": 1}");
if (!i2c) {
fprintf(stderr, "decoder: %s\n", ss_last_error());
return 1;
}
/* What the trigger takes, as the script describes it (a JSON array). */
char *options = ss_decoder_trigger_options(workspace, i2c);
printf("%s\n", options);
ss_string_free(options);
/* trig_addr alone selects the "on I2C address" alternative; 50 % before it. */
char *trigger = ss_decoder_trigger(workspace, i2c, "{\"trig_addr\": \"0x41\"}", 0.5, NULL);
if (!trigger) {
fprintf(stderr, "trigger: %s\n", ss_last_error());
return 1;
}
char request[4096];
snprintf(request, sizeof request,
"{\"samples\": 2000000, \"sample_rate\": 25000000, \"trigger\": %s}", trigger);
ss_string_free(trigger);
if (ss_capture_run(workspace, request, 300.0) < 0) {
fprintf(stderr, "capture: %s\n", ss_last_error());
return 1;
}
ss_decoder_wait(workspace, i2c, 300.0);
/* Where the trigger fired: the T marker. */
int64_t t = 0;
ss_markers *markers = ss_markers_read(workspace);
if (markers) {
size_t n = ss_markers_count(markers);
ss_marker *list = malloc(sizeof *list * n);
ss_markers_copy(markers, 0, list, n);
for (size_t i = 0; i < n; i += 1) {
if (list[i].trigger) t = list[i].sample;
}
free(list);
ss_markers_free(markers);
}
/* The decoded items within 1 ms of it. */
ss_items *batch = ss_items_read_instance(workspace, i2c, t - 25000, t + 25000, 0);
if (batch) {
size_t count = ss_items_count(batch);
ss_item *items = malloc(sizeof *items * count);
ss_items_copy(batch, 0, items, count);
for (size_t i = 0; i < count; i += 1) {
printf("%lld %s\n", (long long)items[i].start, items[i].content);
}
free(items);
ss_items_free(batch);
}
ss_workspace_close(workspace);
ss_workspace_free(workspace);
ss_server_disconnect(server);
return 0;
}
What it prints
On an SP259 with an I²C master writing to 0x22, 0x40, 0x43 and then 0x41:
trig_alt: Select trigger type (selectable_containers) one of ['Trigger on any frame', 'on I2C address'] = 'on I2C address'
trig_any_frame [Trigger on any frame]: Trigger on: (combo) one of ['Valid start condition', 'Valid stop condition', 'Any unacknowledged address', 'Any acknowledged address'] = 'Valid start condition'
trig_addr [on I2C address]: Slave address (text_input) = ''
trig_access_type [on I2C address]: Access type (combo) one of ['Any (read or write)', 'Read', 'Write'] = 'Any (read or write)'
trig_chk_ack [on I2C address]: Address must be acknowledged by a slave (checkbox) = False
triggered at 40.000 ms
39.973 ms START
39.981 ms Write to 0x41 - R/W = 0
40.005 ms NACK
40.040 ms STOP
The three other addresses go by without a trigger. The trigger fires right after the read/write bit of the 0x41 frame, as soon as the address is certain and before the acknowledge.
A demo device captures at once, whatever the trigger says, and the frame at the trigger is whatever its noise decodes to. Point the program at real hardware.
UART: a byte or a string
The same three calls with uart.js. The byte can be written 0x5A, 90 or
'Z':
- Python
- NodeJS
- Rust
- C
uart = workspace.decoders.add("uart.js", {"ch": 0, "baud": 115_200})
trigger = uart.trigger({"trig_byte": "'Z'"}, position=0.5)
# or a string: uart.trigger({"trig_phrase": "OK"}, position=0.5)
const uart = await workspace.decoders.add('uart.js', { ch: 0, baud: 115_200 });
const trigger = await uart.trigger({ trig_byte: "'Z'" }, { position: 0.5 });
// or a string: await uart.trigger({ trig_phrase: 'OK' }, { position: 0.5 });
let uart = workspace.decoders()
.add("uart.js", &values([("ch", 0.into()), ("baud", 115_200.into())]))
.await?;
let trigger = workspace.decoders()
.trigger(uart.instance_id, &values([("trig_byte", "'Z'".into())]))
.await?
.at(0.5);
uint32_t uart = ss_decoder_add(workspace, "uart.js", "{\"ch\": 0, \"baud\": 115200}");
char *trigger = ss_decoder_trigger(workspace, uart, "{\"trig_byte\": \"'Z'\"}", 0.5, NULL);
Notes for your own bench
Ask the decoder what its trigger takes. trigger_options() lists every
option with its caption, its type, its choices and its default, read from the
script itself. The same list makes a good schema for a form, a test sequencer or
an AI agent: it can fill the trigger in without knowing how the protocol looks
on the wire.
An option selects its own alternative. Many trigger dialogs start with a
choice, such as uart.js's "any valid frame", "specific word" or "characters
string" (trig_alt). Giving trig_byte selects "specific word" for you; give
trig_alt yourself, by index or caption, only to pick an alternative that takes
no option. This needs SDK 0.2.13 or newer.
Not every decoder builds a trigger. decoder.can_trigger says whether its
script does. Asking one that does not for a trigger raises ScriptError.
The trigger follows the decoder. It is built again at every capture start,
from the decoder's configuration of the moment: change the decoder's baud rate
or channels with update(), and the next capture's trigger follows. Remove the
decoder, and the capture refuses the trigger.
Leave room for the frame. The sample rate must resolve the protocol: 25
samples per bit or more is comfortable, so 25 MS/s for 1 MHz I²C or 1 Mbaud
UART. position=0.5 keeps as much before the trigger as after it.
UART has no frame sync. A UART trigger is exact on bytes with idle time between them, which is how most firmware sends them. In a continuous stream of bytes, the tail of one byte and the head of the next can read as a third one. When it matters, check the decoded byte at the trigger and re-arm.
There is no protocol logic in the device: the script describes the frame as edges and timing windows, and the device's step engine runs them. Read Triggers for the step model behind it.