Skip to main content

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​

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:]))

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.

Demo devices do not run triggers

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':

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)

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.

The trigger is a step program

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.