SPI analyzer extension with custom decode

I’m trying create an extension that can decode frames with specific devices in mind. These devices read and write in 3 8 bit messages. The first bit is a read/write indicator followed by 6 address bits, 16 data bits and a parity bit. I want to be able to easily determine if the message is r/w and decode the address and data but this is made difficult because the data portion expands 1 bit into the first byte. I’m having trouble figuring out where to start because of the lack of debug options, though I have altered the demo SPI analyzer to generate messages similar to what i expect. Specifically I’m not seeing where i can access the bits to do any operations on them to decode them differently and when looking at the API the AnalyzerFrame object only has these variables exposed. is it even possible to analyze 3 bytes at a time? Any help would be greatly appreciated.

class saleae.analyzers.AnalyzerFrame(type: [str], start_time: saleae.data.timing.GraphTime, end_time: saleae.data.timing.GraphTime, data: [dict] = None)

A frame produced by an analyzer. The types of frames and the fields in each will depend on the analyzer.

  • Variables

  • [type] ([str]) – Frame type

  • start_time (saleae.data.GraphTime) – Start of frame

  • end_time (saleae.data.GraphTime) – End of frame

  • data ([dict]) – Key/value data associated with this frame

Is the frame really 3 x 8 bit, or can it be considered 1 x 24 bit frame containing a number of fields? If you set the “Bits per transfer” to 24 does that help fix the issue?

If it really is 3 separate bytes you can set up your Python decoder object to fetch three bytes and save them before generating an output frame.

It is 3 x 8 bit transmissions, but selecting 24 bits does grab the frames correctly. Does the dict variable contain all frame in the capture? Also would i do manipulate the decoder in the decode() function? Are there any examples of how this would be done?

The SPI - Frame Format documentation describes MOSI and MISO as containing “bytes” which suggests that all the data bits are contained in those properties.

There are a number of SPI decoder extensions in the downloadable Extensions list in Logic 2. You could install one of those and take a look at the code for it to see how it goes about business. On Windows, with a little digging, you can find the .py file for extensions in %appdata%/Logic/Marketplace/nnn where nnn is a numbered folder.

Thanks for the response. I’m almost there but having an issue returning the new frame.

EDIT:
I’ve update the code and can now get an output, but the instance variables are not being passed in the AnalyzerFrame object. If i remove the class variables i get an error stating they do not exit.

Here is my code

class Hla(HighLevelAnalyzer):
    device_setting = ChoicesSetting(choices=('Mixer', 'DAC'))
    
    result_types = {
        'Mixer': {
            'format': '{{task}} @ Address {{address}} Data: {{data}} with parity {{parity}}. '
            # 'format': '{{task}}'
        },
        'DAC': {
            'format': 'Address {{data.address}}'
        }
    }
    
    task = ''
    address = ''
    data = ''
    parity = ''

    def __init__(self):
        print("Settings:", self.device_setting)


    def decode(self, frame: AnalyzerFrame):
        '''
        Process a frame from the input analyzer, and optionally return a single `AnalyzerFrame` or a list of `AnalyzerFrame`s.
        '''
        
        self.task
        self.address
        self.data
        self.parity
        if self.device_setting == 'Mixer':
            if not len(frame.data) == 0:
                print(frame.data)
                if len(frame.data['mosi']) != 3:
                    print('Set analyser Bits Per Transfer to 24')
                    return
                self.address = ((frame.data['mosi'][0] & 0x7E) >> 1).to_bytes(1, 'big').hex().upper()
                parity = (frame.data['mosi'][2] & 0x01)
                if parity == 0:
                    parity = 'Off'
                else:
                    self.parity = 'On'
                print(f'Address {self.address}')
                print(f'Parity {self.parity}')
                if (frame.data['mosi'][0] & 0x80) != 0:
                    self.task = 'Write Mode'
                    print(self.task)
                    self.data = ((frame.data['mosi'][0] & 0x01) << 7)
                    self.data = ((self.data << 8 ) | (frame.data['mosi'][1] << 7) | ((frame.data['mosi'][2] & 0xFE) >> 1)).to_bytes(2, 'big').hex().upper()
                    print(f'Data 0x{self.data}')
                else:
                    self.task = 'Read Mode'
                    print(self.task)
                    self.data = ((frame.data['miso'][0] & 0x01) << 7)
                    self.data = ((self.data << 8 ) | (frame.data['miso'][1] << 7) | ((frame.data['miso'][2] & 0xFE) >> 1)).to_bytes(2, 'big').hex().upper()
                    print(f'Data 0x{self.data}')
            return AnalyzerFrame('Mixer', frame.start_time, frame.end_time, {
                'task': self.task,
                'address': self.address,
                'data': self.data,
                'parity': self.parity
            })

I’m getting the correct data in the terminal

Settings: Mixer
{'mosi': b'\x00\x01\x02', 'miso': b'\x01\x02\x03'}
Address 00
Parity 
Read Mode
Data 0x8101
{'mosi': b'\x03\x04\x05', 'miso': b'\x04\x05\x06'}
Address 01
Parity On
Read Mode
Data 0x0283
{'mosi': b'\x06\x07\x08', 'miso': b'\x07\x08\t'}
Address 03
Parity On
Read Mode
Data 0x8404
{'mosi': b'\t\n\x0b', 'miso': b'\n\x0b\x0c'}
Address 04
Parity On
Read Mode
Data 0x0586
{'mosi': b'\x0c\r\x0e', 'miso': b'\r\x0e\x0f'}
Address 06
Parity On
Read Mode
Data 0x8707
{'mosi': b'\x0f\x10\x11', 'miso': b'\x10\x11\x12'}
Address 07
Parity On
Read Mode
Data 0x0889
{'mosi': b'\x12\x13\x14', 'miso': b'\x13\x14\x15'}
Address 09
Parity On
Read Mode
Data 0x8A0A
{'mosi': b'\x15\x16\x17', 'miso': b'\x16\x17\x18'}
Address 0A
Parity On
Read Mode
Data 0x0B8C
{'mosi': b'\x18\x19\x1a', 'miso': b'\x19\x1a\x1b'}
Address 0C
Parity On
Read Mode
Data 0x8D0D
{'mosi': b'\x1b\x1c\x1d', 'miso': b'\x1c\x1d\x1e'}
Address 0D
Parity On
Read Mode
Data 0x0E8F
{'mosi': b'\x1e\x1f ', 'miso': b'\x1f !'}
Address 0F
Parity On
Read Mode
Data 0x9010
{'mosi': b'!"#', 'miso': b'"#$'}
Address 10
Parity On
Read Mode
Data 0x1192

Nothing jumps out at me as a problem. What was the actual error message you got?

I’m not getting an error, the strings variables are not being passed back in the frame. The rest of the text shows up just not the vars (address, mode, data, parity).

EDIT: Figured out a small part of the issue and adjusted the code. The issue is the format of my returned AnalyzerFrame. i can print the expected message fine before returning the frame.

class Hla(HighLevelAnalyzer):
    device_setting = ChoicesSetting(choices=('Mixer', 'DAC'))

    result_types = {
        'Mixer': {
            'format': '{{task}} @ Address {{address}} --- Data: {{data}} with parity {{parity}}'
            # 'format': '{{task}}'
        },
        'DAC': {
            'format': 'Address {{result.address}}'
        }
    }

    def __init__(self):
        print("Settings:", self.device_setting)

    def decode(self, frame: AnalyzerFrame):
        '''
        Process a frame from the input analyzer, and optionally return a single `AnalyzerFrame` or a list of `AnalyzerFrame`s.
        '''
        if self.device_setting == 'Mixer':
            if not len(frame.data) == 0:
                print(frame.data)
                if len(frame.data['mosi']) != 3:
                    print('Set analyser Bits Per Transfer to 24')
                    return
                self.address = ((frame.data['mosi'][0] & 0x7E) >> 1).to_bytes(1, 'big').hex().upper()
                self.parity = (frame.data['mosi'][2] & 0x01)
                if self.parity == 0:
                    self.parity = 'Off'
                else:
                    self.parity = 'On'
                print(f'Address {self.address}')
                print(f'Parity {self.parity}')
                if (frame.data['mosi'][0] & 0x80) != 0:
                    self.task = 'Write Mode'
                    print(self.task)
                    self.data = ((frame.data['mosi'][0] & 0x01) << 7)
                    self.data = ((self.data << 8 ) | (frame.data['mosi'][1] << 7) | ((frame.data['mosi'][2] & 0xFE) >> 1)).to_bytes(2, 'big').hex().upper()
                    print(f'Data 0x{self.data}')
                else:
                    self.task = 'Read Mode'
                    print(self.task)
                    self.data = ((frame.data['miso'][0] & 0x01) << 7)
                    self.data = ((self.data << 8 ) | (frame.data['miso'][1] << 7) | ((frame.data['miso'][2] & 0xFE) >> 1)).to_bytes(2, 'big').hex().upper()
                    print(f'Data 0x{self.data}')

                print(f'{self.task} @ Address {self.address} --- Data: {self.data} with parity {self.parity}')
                return AnalyzerFrame('Mixer', frame.start_time, frame.end_time, {
                    'task': self.task,
                    'address': self.address,
                    'data': self.data,
                    'parity': self.parity
                })

Figured out the issue. Looks like result_types can only return one variable at a time. I got around it by returning a string and building the string with the variables.

1 Like