]> sigrok.org Git - libsigrokdecode.git/commitdiff
Add 24xx I2C EEPROM protocol decoder.
authorUwe Hermann <redacted>
Wed, 19 Nov 2014 17:57:56 +0000 (18:57 +0100)
committerUwe Hermann <redacted>
Sun, 28 Dec 2014 15:09:07 +0000 (16:09 +0100)
decoders/eeprom24xx/__init__.py [new file with mode: 0644]
decoders/eeprom24xx/lists.py [new file with mode: 0644]
decoders/eeprom24xx/pd.py [new file with mode: 0644]

diff --git a/decoders/eeprom24xx/__init__.py b/decoders/eeprom24xx/__init__.py
new file mode 100644 (file)
index 0000000..de19da4
--- /dev/null
@@ -0,0 +1,26 @@
+##
+## This file is part of the libsigrokdecode project.
+##
+## Copyright (C) 2014 Uwe Hermann <uwe@hermann-uwe.de>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+##
+
+'''
+This decoder stacks on top of the 'i2c' PD and decodes the
+industry standard 24xx series serial EEPROM protocol.
+'''
+
+from .pd import Decoder
diff --git a/decoders/eeprom24xx/lists.py b/decoders/eeprom24xx/lists.py
new file mode 100644 (file)
index 0000000..ff4b3ed
--- /dev/null
@@ -0,0 +1,171 @@
+##
+## This file is part of the libsigrokdecode project.
+##
+## Copyright (C) 2014 Uwe Hermann <uwe@hermann-uwe.de>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+##
+
+#
+# Chip specific properties:
+#
+# - vendor: chip manufacturer
+# - model: chip model
+# - size: total EEPROM size (in number of bytes)
+# - page_size: page size (in number of bytes)
+# - page_wraparound: Whether writes wrap-around at page boundaries
+# - addr_bytes: number of EEPROM address bytes used
+# - addr_pins: number of address pins (A0/A1/A2) on this chip
+# - max_speed: max. supported I²C speed (in kHz)
+#
+chips = {
+    # Generic chip (128 bytes, 8 bytes page size)
+    'generic': {
+        'vendor': '',
+        'model': 'Generic',
+        'size': 128,
+        'page_size': 8,
+        'page_wraparound': True,
+        'addr_bytes': 1,
+        'addr_pins': 3,
+        'max_speed': 400,
+    },
+
+    # Microchip
+    'microchip_24aa65': {
+        'vendor': 'Microchip',
+        'model': '24AA65',
+        'size': 8 * 1024,
+        'page_size': 64, # Actually 8, but there are 8 pages of "input cache"
+        'page_wraparound': True,
+        'addr_bytes': 2,
+        'addr_pins': 3,
+        'max_speed': 400,
+    },
+    'microchip_24lc65': {
+        'vendor': 'Microchip',
+        'model': '24LC65',
+        'size': 8 * 1024,
+        'page_size': 64, # Actually 8, but there are 8 pages of "input cache"
+        'page_wraparound': True,
+        'addr_bytes': 2,
+        'addr_pins': 3,
+        'max_speed': 400,
+    },
+    'microchip_24c65': {
+        'vendor': 'Microchip',
+        'model': '24C65',
+        'size': 8 * 1024,
+        'page_size': 64, # Actually 8, but there are 8 pages of "input cache"
+        'page_wraparound': True,
+        'addr_bytes': 2,
+        'addr_pins': 3,
+        'max_speed': 400,
+    },
+    'microchip_24aa64': {
+        'vendor': 'Microchip',
+        'model': '24AA64',
+        'size': 8 * 1024,
+        'page_size': 32,
+        'page_wraparound': True,
+        'addr_bytes': 2,
+        'addr_pins': 3,
+        'max_speed': 400, # 100 for VCC < 2.5V
+    },
+    'microchip_24lc64': {
+        'vendor': 'Microchip',
+        'model': '24LC64',
+        'size': 8 * 1024,
+        'page_size': 32,
+        'page_wraparound': True,
+        'addr_bytes': 2,
+        'addr_pins': 3,
+        'max_speed': 400,
+    },
+    'microchip_24aa02uid': {
+        'vendor': 'Microchip',
+        'model': '24AA02UID',
+        'size': 256,
+        'page_size': 8,
+        'page_wraparound': True,
+        'addr_bytes': 1,
+        'addr_pins': 0, # Pins A0, A1, A2 not used
+        'max_speed': 400,
+    },
+    'microchip_24aa025uid': {
+        'vendor': 'Microchip',
+        'model': '24AA025UID',
+        'size': 256,
+        'page_size': 16,
+        'page_wraparound': True,
+        'addr_bytes': 1,
+        'addr_pins': 3,
+        'max_speed': 400,
+    },
+    'microchip_24aa025uid_sot23': {
+        'vendor': 'Microchip',
+        'model': '24AA025UID (SOT-23)',
+        'size': 256,
+        'page_size': 16,
+        'page_wraparound': True,
+        'addr_bytes': 1,
+        'addr_pins': 2, # SOT-23 package: A2 not available
+        'max_speed': 400,
+    },
+
+    # Siemens
+    'siemens_slx_24c01': {
+        'vendor': 'Siemens',
+        'model': 'SLx 24C01',
+        'size': 128,
+        'page_size': 8,
+        'page_wraparound': True,
+        'addr_bytes': 1,
+        'addr_pins': 0, # Pins A0, A1, A2 are not connected (NC)
+        'max_speed': 400,
+    },
+    'siemens_slx_24c02': {
+        'vendor': 'Siemens',
+        'model': 'SLx 24C02',
+        'size': 256,
+        'page_size': 8,
+        'page_wraparound': True,
+        'addr_bytes': 1,
+        'addr_pins': 0, # Pins A0, A1, A2 are not connected (NC)
+        'max_speed': 400,
+    },
+
+    # ST
+    'st_m24c01': {
+        'vendor': 'ST',
+        'model': 'M24C01',
+        'size': 128,
+        'page_size': 16,
+        'page_wraparound': True,
+        'addr_bytes': 1,
+        'addr_pins': 3, # Called E0, E1, E2 on this chip.
+        'max_speed': 400,
+    },
+    'st_m24c02': {
+        'vendor': 'ST',
+        'model': 'M24C02',
+        'size': 256,
+        'page_size': 16,
+        'page_wraparound': True,
+        'addr_bytes': 1,
+        'addr_pins': 3, # Called E0, E1, E2 on this chip.
+        'max_speed': 400,
+    },
+}
diff --git a/decoders/eeprom24xx/pd.py b/decoders/eeprom24xx/pd.py
new file mode 100644 (file)
index 0000000..0738c06
--- /dev/null
@@ -0,0 +1,430 @@
+##
+## This file is part of the libsigrokdecode project.
+##
+## Copyright (C) 2014 Uwe Hermann <uwe@hermann-uwe.de>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+##
+
+import sigrokdecode as srd
+from .lists import *
+
+class Decoder(srd.Decoder):
+    api_version = 2
+    id = 'eeprom24xx'
+    name = '24xx EEPROM'
+    longname = '24xx I²C EEPROM'
+    desc = '24xx series I²C EEPROM protocol.'
+    license = 'gplv2+'
+    inputs = ['i2c']
+    outputs = ['eeprom24xx']
+    options = (
+        {'id': 'chip', 'desc': 'Chip', 'default': 'generic',
+            'values': tuple(chips.keys())},
+        {'id': 'addr_counter', 'desc': 'Initial address counter value',
+            'default': 0},
+    )
+    annotations = (
+        # Warnings
+        ('warnings', 'Warnings'),
+        # Bits/bytes
+        ('control-code', 'Control code'),
+        ('address-pin', 'Address pin (A0/A1/A2)'),
+        ('rw-bit', 'Read/write bit'),
+        ('word-addr-byte', 'Word address byte'),
+        ('data-byte', 'Data byte'),
+        # Fields
+        ('control-word', 'Control word'),
+        ('word-addr', 'Word address'),
+        ('data', 'Data'),
+        # Operations
+        ('byte-write', 'Byte write'),
+        ('page-write', 'Page write'),
+        ('cur-addr-read', 'Current address read'),
+        ('random-read', 'Random read'),
+        ('seq-random-read', 'Sequential random read'),
+        ('seq-cur-addr-read', 'Sequential current address read'),
+        ('ack-polling', 'Acknowledge polling'),
+        ('set-bank-addr', 'Set bank address'), # SBA. Only 34AA04.
+        ('read-bank-addr', 'Read bank address'), # RBA. Only 34AA04.
+        ('set-wp', 'Set write protection'), # SWP
+        ('clear-all-wp', 'Clear all write protection'), # CWP
+        ('read-wp', 'Read write protection status'), # RPS
+    )
+    annotation_rows = (
+        ('bits-bytes', 'Bits/bytes', (1, 2, 3, 4, 5)),
+        ('fields', 'Fields', (6, 7, 8)),
+        ('ops', 'Operations', tuple(range(9, 21))),
+        ('warnings', 'Warnings', (0,)),
+    )
+    binary = (
+        ('binary', 'Binary'),
+    )
+
+    def __init__(self, **kwargs):
+        self.reset()
+
+    def start(self):
+        self.out_ann = self.register(srd.OUTPUT_ANN)
+        self.out_bin = self.register(srd.OUTPUT_BINARY)
+        self.chip = chips[self.options['chip']]
+        self.addr_counter = self.options['addr_counter']
+
+    def putb(self, data):
+        self.put(self.ss_block, self.es_block, self.out_ann, data)
+
+    def putbin(self, data):
+        self.put(self.ss_block, self.es_block, self.out_bin, data)
+
+    def putbits(self, bit1, bit2, bits, data):
+        self.put(bits[bit1][1], bits[bit2][2], self.out_ann, data)
+
+    def reset(self):
+        self.state = 'WAIT FOR START'
+        self.packets = []
+        self.bytebuf = []
+        self.is_cur_addr_read = False
+        self.is_random_access_read = False
+        self.is_seq_random_read = False
+        self.is_byte_write = False
+        self.is_page_write = False
+
+    def packet_append(self):
+        self.packets.append([self.ss, self.es, self.cmd, self.databyte, self.bits])
+        if self.cmd in ('DATA READ', 'DATA WRITE'):
+            self.bytebuf.append(self.databyte)
+
+    def hexbytes(self, idx):
+        return ' '.join(['%02X' % b for b in self.bytebuf[idx:]])
+
+    def put_control_word(self, bits):
+        s = ''.join(['%d' % b[0] for b in reversed(bits[4:])])
+        self.putbits(7, 4, bits, [1, ['Control code bits: ' + s,
+            'Control code: ' + s, 'Ctrl code: ' + s, 'Ctrl code', 'Ctrl', 'C']])
+        for i in reversed(range(self.chip['addr_pins'])):
+            self.putbits(i + 1, i + 1, bits,
+                [2, ['Address bit %d: %d' % (i, bits[i + 1][0]),
+                     'Addr bit %d' % i, 'A%d' % i, 'A']])
+        s1 = 'read' if bits[0][0] == 1 else 'write'
+        s2 = 'R' if bits[0][0] == 1 else 'W'
+        self.putbits(0, 0, bits, [3, ['R/W bit: ' + s1, 'R/W', 'RW', s2]])
+        self.putbits(7, 0, bits, [6, ['Control word', 'Control', 'CW', 'C']])
+
+    def put_word_addr(self, p):
+        if self.chip['addr_bytes'] == 1:
+            a = p[1][3]
+            self.put(p[1][0], p[1][1], self.out_ann,
+                [4, ['Word address byte: %02X' % a, 'Word addr byte: %02X' % a,
+                     'Addr: %02X' % a, 'A: %02X' % a, '%02X' % a]])
+            self.put(p[1][0], p[1][1], self.out_ann, [7, ['Word address',
+                     'Word addr', 'Addr', 'A']])
+            self.addr_counter = a
+        else:
+            a = p[1][3]
+            self.put(p[1][0], p[1][1], self.out_ann,
+                [4, ['Word address high byte: %02X' % a,
+                     'Word addr high byte: %02X' % a,
+                     'Addr high: %02X' % a, 'AH: %02X' % a, '%02X' % a]])
+            a = p[2][3]
+            self.put(p[2][0], p[2][1], self.out_ann,
+                [4, ['Word address low byte: %02X' % a,
+                     'Word addr low byte: %02X' % a,
+                     'Addr low: %02X' % a, 'AL: %02X' % a, '%02X' % a]])
+            self.put(p[1][0], p[2][1], self.out_ann, [7, ['Word address',
+                     'Word addr', 'Addr', 'A']])
+            self.addr_counter = (p[1][3] << 8) | p[2][3]
+
+    def put_data_byte(self, p):
+        if self.chip['addr_bytes'] == 1:
+            s = '%02X' % self.addr_counter
+        else:
+            s = '%04X' % self.addr_counter
+        self.put(p[0], p[1], self.out_ann, [5, ['Data byte %s: %02X' % \
+            (s, p[3]), 'Data byte: %02X' % p[3], \
+            'Byte: %02X' % p[3], 'DB: %02X' % p[3], '%02X' % p[3]]])
+
+    def put_data_bytes(self, idx, cls, s):
+        for p in self.packets[idx:]:
+            self.put_data_byte(p)
+            self.addr_counter += 1
+        self.put(self.packets[idx][0], self.packets[-1][1], self.out_ann,
+            [8, ['Data', 'D']])
+        a = ''.join(['%s' % c[0] for c in s.split()]).upper()
+        self.putb([cls, ['%s (%s): %s' % (s, self.addr_and_len(), \
+                  self.hexbytes(self.chip['addr_bytes'])),
+                  '%s (%s)' % (s, self.addr_and_len()), s, a, s[0]]])
+        self.putbin((0, bytes(self.bytebuf[self.chip['addr_bytes']:])))
+
+    def addr_and_len(self):
+        if self.chip['addr_bytes'] == 1:
+            a = '%02X' % self.bytebuf[0]
+        else:
+            a = '%02X%02X' % tuple(self.bytebuf[:2])
+        num_data_bytes = len(self.bytebuf) - self.chip['addr_bytes']
+        d = '%d bytes' % num_data_bytes
+        if num_data_bytes <= 1:
+            d = d[:-1]
+        return 'addr=%s, %s' % (a, d)
+
+    def decide_on_seq_or_rnd_read(self):
+        if len(self.bytebuf) < 2:
+            self.reset()
+            return
+        if len(self.bytebuf) == 2:
+            self.is_random_access_read = True
+        else:
+            self.is_seq_random_read = True
+
+    def put_operation(self):
+        idx = 1 + self.chip['addr_bytes']
+        if self.is_byte_write:
+            # Byte write: word address, one data byte.
+            self.put_word_addr(self.packets)
+            self.put_data_bytes(idx, 9, 'Byte write')
+        elif self.is_page_write:
+            # Page write: word address, two or more data bytes.
+            self.put_word_addr(self.packets)
+            intitial_addr = self.addr_counter
+            self.put_data_bytes(idx, 10, 'Page write')
+            num_bytes_to_write = len(self.packets[idx:])
+            if num_bytes_to_write > self.chip['page_size']:
+                self.putb([0, ['Warning: Wrote %d bytes but page size is '
+                               'only %d bytes!' % (num_bytes_to_write,
+                               self.chip['page_size'])]])
+            page1 = int(intitial_addr / self.chip['page_size'])
+            page2 = int((self.addr_counter - 1) / self.chip['page_size'])
+            if page1 != page2:
+                self.putb([0, ['Warning: Page write crossed page boundary '
+                               'from page %d to %d!' % (page1, page2)]])
+        elif self.is_cur_addr_read:
+            # Current address read: no word address, one data byte.
+            self.put_data_byte(self.packets[1])
+            self.put(self.packets[1][0], self.packets[-1][1], self.out_ann,
+                [8, ['Data', 'D']])
+            self.putb([11, ['Current address read: %02X' % self.bytebuf[0],
+                       'Current address read', 'Cur addr read', 'CAR', 'C']])
+            self.putbin((0, bytes([self.bytebuf[0]])))
+            self.addr_counter += 1
+        elif self.is_random_access_read:
+            # Random access read: word address, one data byte.
+            self.put_control_word(self.packets[idx][4])
+            self.put_word_addr(self.packets)
+            self.put_data_bytes(idx + 1, 12, 'Random access read')
+        elif self.is_seq_random_read:
+            # Sequential random read: word address, two or more data bytes.
+            self.put_control_word(self.packets[idx][4])
+            self.put_word_addr(self.packets)
+            self.put_data_bytes(idx + 1, 13, 'Sequential random read')
+
+    def handle_wait_for_start(self):
+        # Wait for an I²C START condition.
+        if self.cmd not in ('START', 'START REPEAT'):
+            return
+        self.ss_block = self.ss
+        self.state = 'GET CONTROL WORD'
+
+    def handle_get_control_word(self):
+        # The packet after START must be an ADDRESS READ or ADDRESS WRITE.
+        if self.cmd not in ('ADDRESS READ', 'ADDRESS WRITE'):
+            self.reset()
+            return
+        self.packet_append()
+        self.put_control_word(self.bits)
+        self.state = '%s GET ACK NACK AFTER CONTROL WORD' % self.cmd[8]
+
+    def handle_r_get_ack_nack_after_control_word(self):
+        if self.cmd == 'ACK':
+            self.state = 'R GET WORD ADDR OR BYTE'
+        elif self.cmd == 'NACK':
+            self.es_block = self.es
+            self.putb([0, ['Warning: No reply from slave!']])
+            self.reset()
+        else:
+            self.reset()
+
+    def handle_r_get_word_addr_or_byte(self):
+        if self.cmd == 'STOP':
+            self.es_block = self.es
+            self.putb([0, ['Warning: Slave replied, but master aborted!']])
+            self.reset()
+            return
+        elif self.cmd != 'DATA READ':
+            self.reset()
+            return
+        self.packet_append()
+        self.state = 'R GET ACK NACK AFTER WORD ADDR OR BYTE'
+
+    def handle_r_get_ack_nack_after_word_addr_or_byte(self):
+        if self.cmd == 'ACK':
+            self.state = 'R GET RESTART'
+        elif self.cmd == 'NACK':
+            self.is_cur_addr_read = True
+            self.state = 'GET STOP AFTER LAST BYTE'
+        else:
+            self.reset()
+
+    def handle_r_get_restart(self):
+        if self.cmd == 'RESTART':
+            self.state = 'R READ BYTE'
+        else:
+            self.reset()
+
+    def handle_r_read_byte(self):
+        if self.cmd == 'DATA READ':
+            self.packet_append()
+            self.state = 'R GET ACK NACK AFTER BYTE WAS READ'
+        else:
+            self.reset()
+
+    def handle_r_get_ack_nack_after_byte_was_read(self):
+        if self.cmd == 'ACK':
+            self.state = 'R READ BYTE'
+        elif self.cmd == 'NACK':
+            # It's either a RANDOM READ or a SEQUENTIAL READ.
+            self.state = 'GET STOP AFTER LAST BYTE'
+        else:
+            self.reset()
+
+    def handle_w_get_ack_nack_after_control_word(self):
+        if self.cmd == 'ACK':
+            self.state = 'W GET WORD ADDR'
+        elif self.cmd == 'NACK':
+            self.es_block = self.es
+            self.putb([0, ['Warning: No reply from slave!']])
+            self.reset()
+        else:
+            self.reset()
+
+    def handle_w_get_word_addr(self):
+        if self.cmd == 'STOP':
+            self.es_block = self.es
+            self.putb([0, ['Warning: Slave replied, but master aborted!']])
+            self.reset()
+            return
+        elif self.cmd != 'DATA WRITE':
+            self.reset()
+            return
+        self.packet_append()
+        self.state = 'W GET ACK AFTER WORD ADDR'
+
+    def handle_w_get_ack_after_word_addr(self):
+        if self.cmd == 'ACK':
+            self.state = 'W DETERMINE EEPROM READ OR WRITE'
+        else:
+            self.reset()
+
+    def handle_w_determine_eeprom_read_or_write(self):
+        if self.cmd == 'START REPEAT':
+            # It's either a RANDOM ACCESS READ or SEQUENTIAL RANDOM READ.
+            self.state = 'R2 GET CONTROL WORD'
+        elif self.cmd == 'DATA WRITE':
+            self.packet_append()
+            self.state = 'W GET ACK NACK AFTER BYTE WAS WRITTEN'
+        else:
+            self.reset()
+
+    def handle_w_write_byte(self):
+        if self.cmd == 'DATA WRITE':
+            self.packet_append()
+            self.state = 'W GET ACK NACK AFTER BYTE WAS WRITTEN'
+        elif self.cmd == 'STOP':
+            if len(self.bytebuf) < 2:
+                self.reset()
+                return
+            self.es_block = self.es
+            if len(self.bytebuf) == 2:
+                self.is_byte_write = True
+            else:
+                self.is_page_write = True
+            self.put_operation()
+            self.reset()
+        elif self.cmd == 'START REPEAT':
+            # It's either a RANDOM ACCESS READ or SEQUENTIAL RANDOM READ.
+            self.state = 'R2 GET CONTROL WORD'
+        else:
+            self.reset()
+
+    def handle_w_get_ack_nack_after_byte_was_written(self):
+        if self.cmd == 'ACK':
+            self.state = 'W WRITE BYTE'
+        else:
+            self.reset()
+
+    def handle_r2_get_control_word(self):
+        if self.cmd == 'ADDRESS READ':
+            self.packet_append()
+            self.state = 'R2 GET ACK AFTER ADDR READ'
+        else:
+            self.reset()
+
+    def handle_r2_get_ack_after_addr_read(self):
+        if self.cmd == 'ACK':
+            self.state = 'R2 READ BYTE'
+        else:
+            self.reset()
+
+    def handle_r2_read_byte(self):
+        if self.cmd == 'DATA READ':
+            self.packet_append()
+            self.state = 'R2 GET ACK NACK AFTER BYTE WAS READ'
+        elif self.cmd == 'STOP':
+            self.decide_on_seq_or_rnd_read()
+            self.es_block = self.es
+            self.putb([0, ['Warning: STOP expected after a NACK (not ACK)']])
+            self.put_operation()
+            self.reset()
+        else:
+            self.reset()
+
+    def handle_r2_get_ack_nack_after_byte_was_read(self):
+        if self.cmd == 'ACK':
+            self.state = 'R2 READ BYTE'
+        elif self.cmd == 'NACK':
+            self.decide_on_seq_or_rnd_read()
+            self.state = 'GET STOP AFTER LAST BYTE'
+        else:
+            self.reset()
+
+    def handle_get_stop_after_last_byte(self):
+        if self.cmd == 'STOP':
+            self.es_block = self.es
+            self.put_operation()
+            self.reset()
+        elif self.cmd == 'START REPEAT':
+            self.es_block = self.es
+            self.putb([0, ['Warning: STOP expected (not RESTART)']])
+            self.put_operation()
+            self.reset()
+            self.ss_block = self.ss
+            self.state = 'GET CONTROL WORD'
+        else:
+            self.reset()
+
+    def decode(self, ss, es, data):
+        self.cmd, self.databyte = data
+
+        # Collect the 'BITS' packet, then return. The next packet is
+        # guaranteed to belong to these bits we just stored.
+        if self.cmd == 'BITS':
+            self.bits = self.databyte
+            return
+
+        # Store the start/end samples of this I²C packet.
+        self.ss, self.es = ss, es
+
+        # State machine.
+        s = 'handle_%s' % self.state.lower().replace(' ', '_')
+        handle_state = getattr(self, s)
+        handle_state()