Protocol decoder API/Queries

From sigrok
Revision as of 23:41, 13 November 2020 by Abraxa (talk | contribs) (Elaborate where the re-used v2 API parts are documented)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Note: PD API (v3) is required for all new protocol decoders. Support for the old (v2) PD API has been removed as of libsigrokdecode 0.5.1.

Motivation

The major change in version 3 of the libsigrokdecode PD API is that we're removing the need for the decoder code to loop over every single logic analyzer sample "by hand" in Python (which has performance implications, among other things).

Instead, we now use query-based decoders that are generally written as a state machine that uses the new self.wait() call (see below) to tell the libsigrokdecode backend to skip samples until a certain set of specified conditions is encountered (i.e. the PD sends queries to the backend).

Using such queries in PDs has the performance benefit of not having to (slowly) iterate over every single sample in Python code (this is now done more efficiently in the C backend). Additionally, this allows for further performance improvements in the backend later on, e.g. by using multiple threads to process chunks of samples in parallel when looking for a condition match). Any of these backend changes will be transparent to the PDs.

The Decoder class contains the following special attributes and methods with specific purposes:

self.__init__()

No changes, works the same as in PD API v2.

self.start()

No changes, works the same as in PD API v2.

self.put()

No changes, works the same as in PD API v2.

self.decode()

This is the API call that is used to run the actual protocol decoder.

This method does not take any parameters. It is a blocking call that doesn't return until all samples have been decoded/processed.

The self.decode() method generally consists of a while True: loop that uses one or more self.wait() calls to wait for certain conditions in the data and then decodes/processes the data and advances the PD's state machine as needed.

self.wait()

This is the API call that is used by protocol decoders to send queries to the libsigrokdecode backend.

It is a blocking call from the PD's point of view. It will block until the specified condition(s) are found in the sample data, only then will it return control to the PD.

Syntax

def wait(self, conds):
    # 1. Wait until one or more of the conditions in conds match.
    # 2. Set self.samplenum to the absolute samplenumber of the sample that matched.
    # 3. Set self.matched according to which of the conditions matched.
    # 4. Return a tuple containing the pin values of the sample that matched.

The self.wait() call takes exactly one parameter (conds) as input. This parameter is usually a list of conditions or just a single condition (syntactically slightly nicer than a list containing just one condition). Each entry in the list is a condition that the PD wants to wait for. Each condition is a Python dict consisting of one or more terms, a.k.a. key-value pairs (see below).

If multiple conditions are provided in conds, they are logically OR'd. That means e.g. self.wait([cond1, cond2, cond3]) will return when either cond1 and/or cond2 and/or cond3 yield a match in the sample data. I.e., either one or more than one of the specified conditions can match. If none of the conditions match, the self.wait() call will not return.

The self.wait() call will return after the first match is found and will not try to find any other matches afterwards. If the decoder wants to wait for further conditions, it has to call self.wait() with the desired condition(s) again.

The general usage is as follows:

# Wait until at least one of the specified conditions match.
pins = self.wait([cond1, cond2, cond3, ...])

If there is only one condition to wait for, a syntactically nicer form can also be used:

# Wait until the specified condition matches.
pins = self.wait([cond1])
pins = self.wait(cond1) # Nicer syntax

If conds is not supplied at all, or if it is an empty list [], or if it is just an "empty" condition {}, then the backend will simply skip to the next sample.

Examples:

# Don't wait for any condition, just skip to the next sample.
pins = self.wait()
pins = self.wait([])
pins = self.wait({})
pins = self.wait({'skip': 1}) # Skip one sample, see below.

Note: The self.wait() variant is recommended for use in PDs, it's the shortest and nicest version.

Return value:

The self.wait() call always returns one single value, a tuple containing the pin states (0/1 for low/high) of all channels of this specific decoder. The list of entries in the tuple matches the indices/ordering of the channels and optional_channels tuples of the PD.

Examples:

# Example decoder: UART. Optional channels: RX, TX.
rx, tx = self.wait(...)

# Example decoder: JTAG. Channels: TDI, TDO, TCK, TMS. Optional channels: TRST, SRST, RTCK
tdi, tdo, tck, tms, trst, srst, rtck = self.wait(...)

# Alternative (more verbose and usually not recommended):
pins = self.wait(...)
tdi, tdo, tck, tms, trst, srst, rtck = pins

Since self.wait() always returns a tuple of pin values, the call can be conveniently used to pass the resulting pin values on to other methods.

Examples:

# Handle the next rising edge on the CLK pin.
self.handle_rising_clk_edge(self.wait({6: 'r'}))

# Handle the next UART bit.
self.handle_next_uart_bit(self.wait({'skip': self.halfbitwidth}))

# Handle the next I²C START condition (SCL = high, SDA = falling edge).
self.handle_i2c_start(self.wait({0: 'h', 1: 'f'}))

If the decoder doesn't care about the pin values returned by self.wait() it can simply ignore them.

Examples:

# Skip 100 samples. We don't care about the current (new) pin states.
self.wait({'skip': 100})

Conditions

A single condition is always a Python dict which can have zero or more key/value pairs in it.

The keys (and values) can be of different types.

Pin state conditions

The most commonly-used form has keys that are PD channel indices (i.e., integer numbers starting with 0). In those cases, the values can be one of the following:

  • 'l': Low pin value (logical 0)
  • 'h': High pin value (logical 1)
  • 'r': Rising edge
  • 'f': Falling edge
  • 'e': Either edge (rising or falling)
  • 's': Stable state, the opposite of 'e'. That is, there was no edge and the current and previous pin value were both low (or both high).

Any other value will yield an error.

Decoder channels/pins that are not part of a condition will be "don't care", i.e. they will match no matter whether they're high or low or have an edge or not.

Examples:

# Wait until pin 7 has a falling edge.
pins = self.wait({7: 'f'})

# Wait until pin 3 has a rising edge and pin 4 is high at the same time.
pins = self.wait({3: 'r', 4: 'h'})

# Wait until pins 2-4 are low and pin 16 has any edge.
pins = self.wait({2: 'l', 3: 'l', 4: 'l', 16: 'e'})

Sample skipping conditions

Another common query for the backend is when a decoder wants to skip a certain number of samples regardless of what the respective sample values are (because they are not relevant for the protocol at hand).

This can be done with a special key in a condition dict, 'skip'. The value of the 'skip' key is an integer number of samples to skip.

A decoder can also skip a certain amount of time by using the samplerate to calculate the correct value for the 'skip' key.

Examples:

# Skip over the next 100 samples.
pins = self.wait({'skip': 100})

# Skip over the next 20ms of samples.
pins = self.wait({'skip': 20 * (1000 / self.samplerate)})

# Skip half a bitwidth of samples (e.g. for UART).
self.halfbitwidth = int((self.samplerate / self.options['baudrate']) / 2.0)
pins = self.wait({'skip': self.halfbitwidth})

Note: There is intentionally no skip_time (or similar) short-hand key. By having only one skip key for skipping both a number of samples and (indirectly) a certain amount of time, the decoders can more easily construct multi-condition queries in a generic way at runtime.

It is also possible (though rarely needed) to skip forward to a certain absolute sample number:

# Skip forward to the (absolute) sample number 1500.
# The current sample number is self.samplenum.
if self.samplenum <= 1500:
    self.wait({'skip': 1500 - self.samplenum})
else:
    # Error, already past sample 1500.

No skipping at all:

# Skip forward by 0 samples (this is basically a NOP).
self.wait({'skip': 0})

Mixing channel index keys and 'skip' keys in the same condition doesn't usually make much sense:

# Wait until there is an edge on pin 7 and until (at the same time) 1000
# samples passed by since the start of the self.wait() call.
pins = self.wait({7: 'e', 'skip': 1000}) # Not too useful.

However, it can make perfect sense to mix index keys and 'skip' keys in different conditions:

# Wait until there's
# a) an edge on pin 7 and a low state on pin 12, and/or
# b) 1000 samples passed by,
# whichever occurs first (both conditions could occur at the same time too).
# This is basically "wait for an edge on pin 7 and a low state on pin 12,
# with a timeout of 1000 samples".
pins = self.wait([{7: 'e', 12: 'l'}, {'skip': 1000}])

Initial pin values

Frontends (and thus the users) can specify a list of initial pin states (0, 1, or "use the same value as in the first sample") that are assumed to apply to the respective logic analyzer pins/channels before the first sample is passed to the decoder.

These pin values are used when the very first condition that the decoder wants to wait for contains one or more edges. In order for the backend to be able to properly handle a "wait for a rising edge on pin xyz" condition when it looks at the very first sample, it needs to know what the (assumed) value of the sample before the first one is.

self.matched

When a decoder asks the frontend to wait for multiple conditions via self.wait(), when that call returns the PD only knows that at least one of the conditions matched. However, in most cases it also needs to know which of those conditions matched (or did not match).

This is the information that self.matched provides. It is a tuple of boolean values (True or False) that always contains as many entries as there were conditions in the last self.wait() call. For each condition, the respective boolean value denotes whether there was a match for this specific condition.

Example:

# Wait until a rising edge on pin 9 and/or a high state (logic 1) on pin 27,
# and/or a certain amount of "time" has passed (here: 1000 samples skipped).
# That means there's basically a "timeout" of 1000 samples after which
# self.wait() will return for sure (regardless of the other conditions).
pins = self.wait([{9: 'r'}, {27: 'h'}, {'skip': 1000}])

if self.matched == (True, True, False):
    # The first two conditions matched at the same time/sample.
    # Pin 9 contains a rising edge and pin 27 is high.
elif self.matched == (True, False, False):
    # Rising edge on pin 9, pin 27 is guaranteed to not be high.
elif self.matched == (False, True, False):
    # Pin 27 is high, pin 9 is guaranteed to not be a rising edge.
elif self.matched == (False, False, True):
    # Pin 9 is not a rising edge, pin 27 is not high, but 1000 samples were skipped.
elif self.matched == (False, True, True):
    # Pin 9 is not a rising edge, pin 27 is high, and it just so happens that
    # exactly 1000 samples were skipped.
elif self.matched == (False, False, False):
    # Bug, this cannot happen. self.wait() only returns upon >= 1 matches.

For 'skip' key/value pairs the self.matched tuple will contain a True value if the specified number of samples was reached.

Example:

# Wait for a falling edge on channel 18, or until 25000 samples passed by.
pins = self.wait([{18: 'f'}, {'skip': 25000}])

if self.matched[0]:
    # Pin 18 has a falling edge.
if self.matched[1]:
    # 25000 samples were skipped.

self.samplenum

self.samplenum is a special attribute that is read-only for the protocol decoder and should only be set by the libsigrokdecode backend.

The value of self.samplenum is always the current absolute sample number (starts at 0) after the last self.wait() call has returned.

Example: After a self.wait({5: 'r'}) call has returned, self.samplenum will contain the absolute sample number of the sample where pin 5 of this protocol decoder has changed from 0 to 1 (rising edge). The current sample will have a pin 5 value of 1, and the sample before that is guaranteed to have had a pin 5 value of 0.