Difference between revisions of "Protocol decoder HOWTO"

From sigrok
Jump to navigation Jump to search
Line 102: Line 102:
== <tt>channels</tt> & <tt>optional_channels</tt> ==
== <tt>channels</tt> & <tt>optional_channels</tt> ==


The following excerpt from the [[Protocol_decoder:spi|SPI]] PD shows how to use <tt>channels</tt> and <tt>optional_channels</tt>.
The following excerpt from the [[Protocol_decoder:spi|SPI]] PD shows how to use '''<tt>channels</tt>''' and '''<tt>optional_channels</tt>'''.
To give the user the flexibility to provide only one of the MOSI or MISO signals, it defines only the CLK signal as required,
To decode SPI, the clock signal is always needed, the chip-select signal is optional and only used when provided.
and the other signals as optional:
To give the user the flexibility to provide only one of the MOSI/MISO signals, they are both also defined as optional:
<small>
<small>
<pre>
<pre>
Line 119: Line 119:
         {'id': 'cs', 'name': 'CS#', 'desc': 'Chip-select'},
         {'id': 'cs', 'name': 'CS#', 'desc': 'Chip-select'},
     )
     )
</pre>
</small>
'''<tt>data</tt>''', the argument of the decoders [[Protocol decoder API#decode-function|'''<tt>decode()</tt>''']] function that
contains the data to decode, is a list of tuples.
These tuples contain the number of the sample and the data at that sample.
To process all samples, the SPI decoder loops over '''<tt>data</tt>''' like this:
<small>
<pre>
    def decode(self, ss, es, data):
        ...
        for (self.samplenum, pins) in data:
</pre>
</small>
'''<tt>channels</tt>''' and '''<tt>optional_channels</tt>''' contain in total four channels,
therefore the second member of the tupel is an object of Python's [https://docs.python.org/3/library/stdtypes.html#typebytes '''<tt>bytes</tt>'''] class containing 4 bytes, one for each channel.
The decoder unpacks the bytes into the variables
'''<tt>clk</tt>''', '''<tt>miso</tt>''', '''<tt>mosi</tt>''', and '''<tt>cs</tt>'''
as shown below.
Then, it checks for the optional channels, if their value is either 0 or 1.
If it is not, that optional channel is not provided to the decoder.
In the case that neither of them is supplied, an exception is raised:
<small>
<pre>
            (clk, miso, mosi, cs) = pins
            self.have_miso = (miso in (0, 1))
            self.have_mosi = (mosi in (0, 1))
            self.have_cs = (cs in (0, 1))
            # Either MISO or MOSI (but not both) can be omitted.
            if not (self.have_miso or self.have_mosi):
                raise MissingDataError('Either MISO or MOSI (or both) pins required.')


            ...
</pre>
</pre>
</small>
</small>

Revision as of 19:38, 24 July 2014

This page serves as a quick-start guide for people who want to write their own libsigrokdecode protocol decoders (PDs).

It is not intended to replace the Protocol decoder API page, but rather to give a short overview/tutorial and some tips.

Introduction

Protocol decoders are written entirely in Python (>= 3.0).

Files

Every protocol decoder is a Python module and has its own subdirectory in libsigrokdecode's decoders directory.

This is a minimalistic example of how a protocol decoder looks like, in this case the i2c decoder (license header, comments, and some other parts omitted).

Note: Do not start new protocol decoders by copying code from here. Instead, it's recommended to select an already existing decoder in the source code which is similar to the one you plan to write, and copy that as a starting point.

__init__.py

 '''
 I²C (Inter-Integrated Circuit) is a bidirectional, multi-master
 bus using two signals (SCL = serial clock line, SDA = serial data line).

 <Insert notes and hints for the user here>
 '''
 
 from .pd import *

This is a standard Python file, required in every Python module. It contains a module-level docstring, which is accessible by frontends via the libsigrokdecode API. It should contain a (very) short description of what the protocol (in this case I²C) is about, and some notes and hints for the user of this protocol decoder (which can be shown in GUIs when the user selects/browses different PDs).

This docstring should not contain the full, extensive protocol description. Instead, the per-PD wiki page should be used for protocol description, photos of devices or photos of example acquisition setups, and so on. Each decoder has one unique wiki page at the URL http://sigrok.org/wiki/Protocol_decoder:<pd>, where <pd> is the Python module name of the decoder (i2c in this case). Some examples for such per-PD wiki pages: UART, PAN1321, MX25Lxx05D, DCF77.

The "from .pd import *" line will make sure the code from pd.py gets properly imported when this module is used.

pd.py

 import sigrokdecode as srd
 
 class Decoder(srd.Decoder):
     api_version = 2
     id = 'i2c'
     name = 'I²C'
     longname = 'Inter-Integrated Circuit'
     desc = 'Two-wire, multi-master, serial bus.'
     license = 'gplv2+'
     inputs = ['logic']
     outputs = ['i2c']
     channels = (
         {'id': 'scl', 'name': 'SCL', 'desc': 'Serial clock line'},
         {'id': 'sda', 'name': 'SDA', 'desc': 'Serial data line'},
     )
     optional_channels = ()
     options = (
         {'id': 'address_format', 'desc': 'Displayed slave address format',
            'default': 'shifted', 'values': ('shifted', 'unshifted')},
     )
     annotations = (
         ('start', 'Start condition'),
         ('repeat-start', 'Repeat start condition'),
         ('stop', 'Stop condition'),
         ('ack', 'ACK'),
         ('nack', 'NACK'),
         ('bit', 'Data/address bit'),
         ('address-read', 'Address read'),
         ('address-write', 'Address write'),
         ('data-read', 'Data read'),
         ('data-write', 'Data write'),
         ('warnings', 'Human-readable warnings'),
     )
     annotation_rows = (
         ('bits', 'Bits', (5,)),
         ('addr-data', 'Address/Data', (0, 1, 2, 3, 4, 6, 7, 8, 9)),
         ('warnings', 'Warnings', (10,)),
     )
 
     def __init__(self, **kwargs):
         self.state = 'FIND START'
         # And various other variable initializations...
 
     def metadata(self, key, value):
         if key == srd.SRD_CONF_SAMPLERATE:
             self.samplerate = value
 
     def start(self):
         self.out_ann = self.register(srd.OUTPUT_ANN)
 
     def decode(self, ss, es, data):
         for self.samplenum, (scl, sda) in data:
             # Decode the samples.

The recommended name for the actual decoder file is pd.py. This file contains some meta information about the decoder, and the actual code itself, mostly in the decode() method.

If needed, large unwieldy lists or similar things can also be factored out into another *.py file (examples: midi, z80).

channels & optional_channels

The following excerpt from the SPI PD shows how to use channels and optional_channels. To decode SPI, the clock signal is always needed, the chip-select signal is optional and only used when provided. To give the user the flexibility to provide only one of the MOSI/MISO signals, they are both also defined as optional:

 class Decoder(srd.Decoder):
     ...
     id = 'spi'
     ...
     channels = (
         {'id': 'clk', 'name': 'CLK', 'desc': 'Clock'},
     )
     optional_channels = (
         {'id': 'miso', 'name': 'MISO', 'desc': 'Master in, slave out'},
         {'id': 'mosi', 'name': 'MOSI', 'desc': 'Master out, slave in'},
         {'id': 'cs', 'name': 'CS#', 'desc': 'Chip-select'},
     )

data, the argument of the decoders decode() function that contains the data to decode, is a list of tuples. These tuples contain the number of the sample and the data at that sample. To process all samples, the SPI decoder loops over data like this:

     def decode(self, ss, es, data):
         ...
         for (self.samplenum, pins) in data:

channels and optional_channels contain in total four channels, therefore the second member of the tupel is an object of Python's bytes class containing 4 bytes, one for each channel. The decoder unpacks the bytes into the variables clk, miso, mosi, and cs as shown below. Then, it checks for the optional channels, if their value is either 0 or 1. If it is not, that optional channel is not provided to the decoder. In the case that neither of them is supplied, an exception is raised:

             (clk, miso, mosi, cs) = pins
             self.have_miso = (miso in (0, 1))
             self.have_mosi = (mosi in (0, 1))
             self.have_cs = (cs in (0, 1))

             # Either MISO or MOSI (but not both) can be omitted.
             if not (self.have_miso or self.have_mosi):
                 raise MissingDataError('Either MISO or MOSI (or both) pins required.')

             ...

annotations & annotation_rows

To make the relation between the annotations and the annotation_rows members of a decoder object more clear, take a look at how the ir_nec PD uses them:

 class Decoder(srd.Decoder):
     ...
     id = 'ir_nec'
     ...
     annotations = (
         ('bit', 'Bit'),
         ('agc-pulse', 'AGC pulse'),
         ('longpause', 'Long pause'),
         ('shortpause', 'Short pause'),
         ('stop-bit', 'Stop bit'),
         ('leader-code', 'Leader code'),
         ('addr', 'Address'),
         ('addr-inv', 'Address#'),
         ('cmd', 'Command'),
         ('cmd-inv', 'Command#'),
         ('repeat-code', 'Repeat code'),
         ('remote', 'Remote'),
         ('warnings', 'Warnings'),
     )
     annotation_rows = (
         ('bits', 'Bits', (0, 1, 2, 3, 4)),
         ('fields', 'Fields', (5, 6, 7, 8, 9, 10)),
         ('remote', 'Remote', (11,)),
         ('warnings', 'Warnings', (12,)),
     )

It groups the first five annotation types together into the bits row and the next six into the fields row. The rows remote and warnings both only contain one annotation type.

Without annotation_rows, PulseView would put each annotation type in its own row, which is unhandy if the decoder has many annotations, but because of the annotation_rows, the output of the ir_nec decoder is grouped together as shown in the following picture (note how different annotation types, distinguishable by their different colors, share the same row):

Pv example ir nec cropped.png

Random notes, tips and tricks

  • You should usually only use raise in a protocol decoder to raise exceptions in cases which are a clear bug in how the protocol decoder is invoked (e.g. if no samplerate was provided for a PD which needs the samplerate, or if some of the required channels were not provided by the user, and so on).
  • A simple and fast way to calculate a parity (i.e., count the number of 1 bits) over a number (0x55 in this example) is: ones = bin(0x55).count('1').
  • A simple function to convert a BCD number (max. 8 bits) to an integer is: def bcd2int(b): return (b & 0x0f) + ((b >> 4) * 10).
  • A nice way to construct method names according to (e.g.) protocol commands is: fn = getattr(self, 'handle_cmd_0x%02x' % cmd); fn(arg1, arg2, ...).