]> sigrok.org Git - libsigrokdecode.git/commitdiff
ac97: introduce first AC'97 decoder implementation (bits, slots, binary)
authorGerhard Sittig <redacted>
Sat, 2 Sep 2017 16:48:03 +0000 (18:48 +0200)
committerUwe Hermann <redacted>
Thu, 25 Jan 2018 20:05:03 +0000 (21:05 +0100)
Introduce an "audio and modem control for PC systems" protocol decoder
(referred to as AC'97).

This implementation extracts bits and identifies frames, and annotates
the slots of a frame with mere integer values. Bit fields get decoded
depending on the slot numbers. Bit patterns in audio/modem data slots
can get exported as binary streams.

Some TODO items remain. Register access (read/write) gets annotated, but
neither gets interpreted nor affects the decoding of subsequent frames.
The RESET# line status does not get evaluated.

decoders/ac97/__init__.py [new file with mode: 0644]
decoders/ac97/pd.py [new file with mode: 0644]

diff --git a/decoders/ac97/__init__.py b/decoders/ac97/__init__.py
new file mode 100644 (file)
index 0000000..8b96e8a
--- /dev/null
@@ -0,0 +1,36 @@
+##
+## This file is part of the libsigrokdecode project.
+##
+## Copyright (C) 2017 Gerhard Sittig <gerhard.sittig@gmx.net>
+##
+## 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, see <http://www.gnu.org/licenses/>.
+##
+
+'''
+AC'97 (Audio Codec '97) was specifically designed by Intel for audio and
+modem I/O functionality in mainstream PC systems. See the specification in
+http://download.intel.com/support/motherboards/desktop/sb/ac97_r23.pdf
+
+AC'97 communicates full duplex data (SDATA_IN, SDATA_OUT), where bits
+are clocked by the BIT_CLK and frames are signalled by the SYNC signals.
+A low active RESET# line completes the set of signals.
+
+Frames repeat at a nominal frequency of 48kHz, and consist of 256 bits
+each. One 16bit slot contains management information, twelve 20bit slots
+follow which carry data for three management and nine audio/modem channels.
+Optionally two slots of one frame can get combined for higher resolution
+on fewer channels, or double data rate.
+'''
+
+from .pd import Decoder
diff --git a/decoders/ac97/pd.py b/decoders/ac97/pd.py
new file mode 100644 (file)
index 0000000..6cb7e93
--- /dev/null
@@ -0,0 +1,503 @@
+##
+## This file is part of the libsigrokdecode project.
+##
+## Copyright (C) 2017 Gerhard Sittig <gerhard.sittig@gmx.net>
+##
+## 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, see <http://www.gnu.org/licenses/>.
+##
+
+# This implementation is incomplete. TODO items:
+# - Support the optional RESET# pin, detect cold and warm reset.
+# - Split slot values into audio samples of their respective width and
+#   frequency (either on user provided parameters, or from inspection of
+#   decoded register access).
+
+import sigrokdecode as srd
+
+class ChannelError(Exception):
+    pass
+
+class Pins:
+    (SYNC, BIT_CLK, SDATA_OUT, SDATA_IN, RESET) = range(5)
+
+class Ann:
+    (
+        BITS_OUT, BITS_IN,
+        SLOT_OUT_RAW, SLOT_OUT_TAG, SLOT_OUT_ADDR, SLOT_OUT_DATA,
+        SLOT_OUT_03, SLOT_OUT_04, SLOT_OUT_05, SLOT_OUT_06,
+        SLOT_OUT_07, SLOT_OUT_08, SLOT_OUT_09, SLOT_OUT_10,
+        SLOT_OUT_11, SLOT_OUT_IO,
+        SLOT_IN_RAW, SLOT_IN_TAG, SLOT_IN_ADDR, SLOT_IN_DATA,
+        SLOT_IN_03, SLOT_IN_04, SLOT_IN_05, SLOT_IN_06,
+        SLOT_IN_07, SLOT_IN_08, SLOT_IN_09, SLOT_IN_10,
+        SLOT_IN_11, SLOT_IN_IO,
+        WARN, ERROR,
+    ) = range(32)
+    (
+        BIN_FRAME_OUT,
+        BIN_FRAME_IN,
+        BIN_SLOT_RAW_OUT,
+        BIN_SLOT_RAW_IN,
+    ) = range(4)
+
+class Decoder(srd.Decoder):
+    api_version = 3
+    id = 'ac97'
+    name = "AC '97"
+    longname = "Audio Codec '97"
+    desc = 'Audio and modem control for PC systems.'
+    license = 'gplv2+'
+    inputs = ['logic']
+    outputs = ['ac97']
+    channels = (
+        {'id': 'sync', 'name': 'SYNC', 'desc': 'Frame synchronization'},
+        {'id': 'clk', 'name': 'BIT_CLK', 'desc': 'Data bits clock'},
+    )
+    optional_channels = (
+        {'id': 'out', 'name': 'SDATA_OUT', 'desc': 'Data output'},
+        {'id': 'in', 'name': 'SDATA_IN', 'desc': 'Data input'},
+        {'id': 'rst', 'name': 'RESET#', 'desc': 'Reset line'},
+    )
+    annotations = (
+        ('bit-out', 'Output bits'),
+        ('bit-in', 'Input bits'),
+        ('slot-out-raw', 'Output raw value'),
+        ('slot-out-tag', 'Output TAG'),
+        ('slot-out-cmd-addr', 'Output command address'),
+        ('slot-out-cmd-data', 'Output command data'),
+        ('slot-out-03', 'Output slot 3'),
+        ('slot-out-04', 'Output slot 4'),
+        ('slot-out-05', 'Output slot 5'),
+        ('slot-out-06', 'Output slot 6'),
+        ('slot-out-07', 'Output slot 7'),
+        ('slot-out-08', 'Output slot 8'),
+        ('slot-out-09', 'Output slot 9'),
+        ('slot-out-10', 'Output slot 10'),
+        ('slot-out-11', 'Output slot 11'),
+        ('slot-out-io-ctrl', 'Output I/O control'),
+        ('slot-in-raw', 'Input raw value'),
+        ('slot-in-tag', 'Input TAG'),
+        ('slot-in-sts-addr', 'Input status address'),
+        ('slot-in-sts-data', 'Input status data'),
+        ('slot-in-03', 'Input slot 3'),
+        ('slot-in-04', 'Input slot 4'),
+        ('slot-in-05', 'Input slot 5'),
+        ('slot-in-06', 'Input slot 6'),
+        ('slot-in-07', 'Input slot 7'),
+        ('slot-in-08', 'Input slot 8'),
+        ('slot-in-09', 'Input slot 9'),
+        ('slot-in-10', 'Input slot 10'),
+        ('slot-in-11', 'Input slot 11'),
+        ('slot-in-io-sts', 'Input I/O status'),
+        # TODO: Add more annotation classes:
+        # TAG: 'ready', 'valid', 'id', 'rsv'
+        # CMD ADDR: 'r/w', 'addr', 'unused'
+        # CMD DATA: 'data', 'unused'
+        # 3-11: 'data', 'unused', 'double data'
+        ('warning', 'Warning'),
+        ('error', 'Error'),
+    )
+    annotation_rows = (
+        ('bits-out', 'Output bits', (Ann.BITS_OUT,)),
+        ('slots-out-raw', 'Output numbers', (Ann.SLOT_OUT_RAW,)),
+        ('slots-out', 'Output slots', (
+            Ann.SLOT_OUT_TAG, Ann.SLOT_OUT_ADDR, Ann.SLOT_OUT_DATA,
+            Ann.SLOT_OUT_03, Ann.SLOT_OUT_04, Ann.SLOT_OUT_05, Ann.SLOT_OUT_06,
+            Ann.SLOT_OUT_07, Ann.SLOT_OUT_08, Ann.SLOT_OUT_09, Ann.SLOT_OUT_10,
+            Ann.SLOT_OUT_11, Ann.SLOT_OUT_IO,)),
+        ('bits-in', 'Input bits', (Ann.BITS_IN,)),
+        ('slots-in-raw', 'Input numbers', (Ann.SLOT_IN_RAW,)),
+        ('slots-in', 'Input slots', (
+            Ann.SLOT_IN_TAG, Ann.SLOT_IN_ADDR, Ann.SLOT_IN_DATA,
+            Ann.SLOT_IN_03, Ann.SLOT_IN_04, Ann.SLOT_IN_05, Ann.SLOT_IN_06,
+            Ann.SLOT_IN_07, Ann.SLOT_IN_08, Ann.SLOT_IN_09, Ann.SLOT_IN_10,
+            Ann.SLOT_IN_11, Ann.SLOT_IN_IO,)),
+        ('warnings', 'Warnings', (Ann.WARN,)),
+        ('errors', 'Errors', (Ann.ERROR,)),
+    )
+    binary = (
+        ('frame-out', 'Frame bits, output data'),
+        ('frame-in', 'Frame bits, input data'),
+        ('slot-raw-out', 'Raw slot bits, output data'),
+        ('slot-raw-in', 'Raw slot bits, input data'),
+        # TODO: Which (other) binary classes to implement?
+        # - Are binary annotations per audio slot useful?
+        # - Assume 20bit per slot, in 24bit units? Or assume 16bit
+        #   audio samples? Observe register access and derive width
+        #   of the audio data? Dump channels 3-11 or 1-12?
+    )
+
+    def putx(self, ss, es, cls, data):
+        self.put(ss, es, self.out_ann, [cls, data])
+
+    def putf(self, frombit, bitcount, cls, data):
+        ss = self.frame_ss_list[frombit]
+        es = self.frame_ss_list[frombit + bitcount]
+        self.putx(ss, es, cls, data)
+
+    def putb(self, frombit, bitcount, cls, data):
+        ss = self.frame_ss_list[frombit]
+        es = self.frame_ss_list[frombit + bitcount]
+        self.put(ss, es, self.out_binary, [cls, data])
+
+    def __init__(self):
+        self.out_binary = None
+        self.out_ann = None
+        self.reset()
+
+    def reset(self):
+        self.frame_ss_list = None
+        self.frame_slot_lens = [0, 16] + [16 + 20 * i for i in range(1, 13)]
+        self.frame_total_bits = self.frame_slot_lens[-1]
+        self.handle_slots = {
+            0: self.handle_slot_00,
+            1: self.handle_slot_01,
+            2: self.handle_slot_02,
+        }
+
+    def start(self):
+        if not self.out_binary:
+            self.out_binary = self.register(srd.OUTPUT_BINARY)
+        if not self.out_ann:
+            self.out_ann = self.register(srd.OUTPUT_ANN)
+
+    def metadata(self, key, value):
+        if key == srd.SRD_CONF_SAMPLERATE:
+            self.samplerate = value
+
+    def bits_to_int(self, bits):
+        # Convert MSB-first bit sequence to integer value.
+        if not bits:
+            return 0
+        count = len(bits)
+        value = sum([2 ** (count - 1 - i) for i in range(count) if bits[i]])
+        return value
+
+    def bits_to_bin_ann(self, bits):
+        # Convert MSB-first bit sequence to binary annotation data.
+        # It's assumed that the number of bits does not (in useful ways)
+        # fit into an integer, and we need to create an array of bytes
+        # from the data afterwards, anyway. Hence the separate routine
+        # and the conversion of eight bits each.
+        out = []
+        count = len(bits)
+        while count > 0:
+            count -= 8
+            by, bits = bits[:8], bits[8:]
+            by = self.bits_to_int(by)
+            out.append(by)
+        out = bytes(out)
+        return out
+
+    def int_to_nibble_text(self, value, bitcount):
+        # Convert number to hex digits for given bit count.
+        digits = (bitcount + 3) // 4
+        text = '{{:0{:d}x}}'.format(digits).format(value)
+        return text
+
+    def get_bit_field(self, data, size, off, count):
+        shift = size - off - count
+        data >>= shift
+        mask = (1 << count) - 1
+        data &= mask
+        return data
+
+    def flush_frame_bits(self):
+        # Flush raw frame bits to binary annotation.
+        anncls = Ann.BIN_FRAME_OUT
+        data = self.frame_bits_out[:]
+        count = len(data)
+        data = self.bits_to_bin_ann(data)
+        self.putb(0, count, anncls, data)
+
+        anncls = Ann.BIN_FRAME_IN
+        data = self.frame_bits_in[:]
+        count = len(data)
+        data = self.bits_to_bin_ann(data)
+        self.putb(0, count, anncls, data)
+
+    def start_frame(self, ss):
+        # Mark the start of a frame.
+        if self.frame_ss_list:
+            # Flush bits if we had a frame before the frame which is
+            # starting here.
+            self.flush_frame_bits()
+        self.frame_ss_list = [ss]
+        self.frame_bits_out = []
+        self.frame_bits_in = []
+        self.frame_slot_data_out = []
+        self.frame_slot_data_in = []
+        self.have_slots = {True: None, False: None}
+
+    def handle_slot_dummy(self, slotidx, bitidx, bitcount, is_out, data):
+        # Handle slot x, default/fallback handler.
+        # Only process data of slots 1-12 when slot 0 says "valid".
+        if not self.have_slots[is_out]:
+            return
+        if not self.have_slots[is_out][slotidx]:
+            return
+
+        # Emit a naive annotation with just the data bits that we saw
+        # for the slot (hex nibbles for density). For audio data this
+        # can be good enough. Slots with special meaning should not end
+        # up calling the dummy handler.
+        text = self.int_to_nibble_text(data, bitcount)
+        anncls = Ann.SLOT_OUT_TAG if is_out else Ann.SLOT_IN_TAG
+        self.putf(bitidx, bitcount, anncls + slotidx, [text])
+
+        # Emit binary output for the data that is contained in slots
+        # which end up calling the default handler. This transparently
+        # should translate to "the slots with audio data", as other
+        # slots which contain management data should have their specific
+        # handler routines. In the present form, this approach might be
+        # good enough to get a (header-less) audio stream for typical
+        # setups where only line-in or line-out are in use.
+        #
+        # TODO: Improve this early prototype implementation. For now the
+        # decoder just exports the upper 16 bits of each audio channel
+        # that happens to be valid. For an improved implementation, it
+        # either takes user provided specs or more smarts like observing
+        # register access (if the capture includes it).
+        anncls = Ann.BIN_SLOT_RAW_OUT if is_out else Ann.BIN_SLOT_RAW_IN
+        data_bin = data >> 4
+        data_bin &= 0xffff
+        data_bin = data_bin.to_bytes(2, byteorder = 'big')
+        self.putb(bitidx, bitcount, anncls, data_bin)
+
+    def handle_slot_00(self, slotidx, bitidx, bitcount, is_out, data):
+        # Handle slot 0, TAG.
+        slotpos = self.frame_slot_lens[slotidx]
+        fieldoff = 0
+        anncls = Ann.SLOT_OUT_TAG if is_out else Ann.SLOT_IN_TAG
+
+        fieldlen = 1
+        ready = self.get_bit_field(data, bitcount, fieldoff, fieldlen)
+        text = ['READY: 1', 'READY', 'RDY', 'R'] if ready else ['ready: 0', 'rdy', '-']
+        self.putf(slotpos + fieldoff, fieldlen, anncls, text)
+        fieldoff += fieldlen
+
+        fieldlen = 12
+        valid = self.get_bit_field(data, bitcount, fieldoff, fieldlen)
+        text = ['VALID: {:3x}'.format(valid), '{:3x}'.format(valid)]
+        self.putf(slotpos + fieldoff, fieldlen, anncls, text)
+        have_slots = [True] + [False] * 12
+        for idx in range(12):
+            have_slots[idx + 1] = bool(valid & (1 << (11 - idx)))
+        self.have_slots[is_out] = have_slots
+        fieldoff += fieldlen
+
+        fieldlen = 1
+        rsv = self.get_bit_field(data, bitcount, fieldoff, fieldlen)
+        if rsv != 0:
+            text = ['reserved bit error', 'rsv error', 'rsv']
+            self.putf(slotpos + fieldoff, fieldlen, Ann.ERROR, text)
+        fieldoff += fieldlen
+
+        # TODO: Will input slot 0 have a Codec ID, or 3 reserved bits?
+        fieldlen = 2
+        codec = self.get_bit_field(data, bitcount, fieldoff, fieldlen)
+        text = ['CODEC: {:1x}'.format(codec), '{:1x}'.format(codec)]
+        self.putf(slotpos + fieldoff, fieldlen, anncls, text)
+        fieldoff += fieldlen
+
+    def handle_slot_01(self, slotidx, bitidx, bitcount, is_out, data):
+        # Handle slot 1, command/status address.
+        slotpos = self.frame_slot_lens[slotidx]
+        if not self.have_slots[is_out]:
+            return
+        if not self.have_slots[is_out][slotidx]:
+            return
+        fieldoff = 0
+        anncls = Ann.SLOT_OUT_TAG if is_out else Ann.SLOT_IN_TAG
+        anncls += slotidx
+
+        fieldlen = 1
+        if is_out:
+            is_read = self.get_bit_field(data, bitcount, fieldoff, fieldlen)
+            text = ['READ', 'RD', 'R'] if is_read else ['WRITE', 'WR', 'W']
+            self.putf(slotpos + fieldoff, fieldlen, anncls, text)
+            # TODO: Check for the "atomic" constraint? Some operations
+            # involve address _and_ data, which cannot be spread across
+            # several frames. Slot 0 and 1 _must_ be provided within the
+            # same frame (the test should occur in the handler for slot
+            # 2 of course, in slot 1 we don't know what will follow).
+        else:
+            rsv = self.get_bit_field(data, bitcount, fieldoff, fieldlen)
+            if rsv != 0:
+                text = ['reserved bit error', 'rsv error', 'rsv']
+                self.putf(slotpos + fieldoff, fieldlen, Ann.ERROR, text)
+        fieldoff += fieldlen
+
+        fieldlen = 7
+        regaddr = self.get_bit_field(data, bitcount, fieldoff, fieldlen)
+        # TODO: Present 0-63 or 0-126 as the address of the 16bit register?
+        text = ['ADDR: {:2x}'.format(regaddr), '{:2x}'.format(regaddr)]
+        self.putf(slotpos + fieldoff, fieldlen, anncls, text)
+        if regaddr & 0x01:
+            text = ['odd register address', 'odd reg addr', 'odd addr', 'odd']
+            self.putf(slotpos + fieldoff, fieldlen, Ann.ERROR, text)
+        fieldoff += fieldlen
+
+        # Strictly speaking there are 10 data request bits and 2 reserved
+        # bits for input slots, and 12 reserved bits for output slots. We
+        # test for 10 and 2 bits, to simplify the logic. Only in case of
+        # non-zero reserved bits for outputs this will result in "a little
+        # strange" an annotation. This is a cosmetic issue, we don't mind.
+        fieldlen = 10
+        reqdata = self.get_bit_field(data, bitcount, fieldoff, fieldlen)
+        if is_out and reqdata != 0:
+            text = ['reserved bit error', 'rsv error', 'rsv']
+            self.putf(slotpos + fieldoff, fieldlen, Ann.ERROR, text)
+        if not is_out:
+            text = ['REQ: {:3x}'.format(reqdata), '{:3x}'.format(reqdata)]
+            self.putf(slotpos + fieldoff, fieldlen, anncls, text)
+        fieldoff += fieldlen
+
+        fieldlen = 2
+        rsv = self.get_bit_field(data, bitcount, fieldoff, fieldlen)
+        if rsv != 0:
+            text = ['reserved bit error', 'rsv error', 'rsv']
+            self.putf(slotpos + fieldoff, fieldlen, Ann.ERROR, text)
+        fieldoff += fieldlen
+
+    def handle_slot_02(self, slotidx, bitidx, bitcount, is_out, data):
+        # Handle slot 2, command/status data.
+        slotpos = self.frame_slot_lens[slotidx]
+        if not self.have_slots[is_out]:
+            return
+        if not self.have_slots[is_out][slotidx]:
+            return
+        fieldoff = 0
+        anncls = Ann.SLOT_OUT_TAG if is_out else Ann.SLOT_IN_TAG
+        anncls += slotidx
+
+        fieldlen = 16
+        rwdata = self.get_bit_field(data, bitcount, fieldoff, fieldlen)
+        # TODO: Check for zero output data when the operation is a read.
+        # TODO: Check for the "atomic" constraint.
+        text = ['DATA: {:4x}'.format(rwdata), '{:4x}'.format(rwdata)]
+        self.putf(slotpos + fieldoff, fieldlen, anncls, text)
+        fieldoff += fieldlen
+
+        fieldlen = 4
+        rsv = self.get_bit_field(data, bitcount, fieldoff, fieldlen)
+        if rsv != 0:
+            text = ['reserved bits error', 'rsv error', 'rsv']
+            self.putf(slotpos + fieldoff, fieldlen, Ann.ERROR, text)
+        fieldoff += fieldlen
+
+    # TODO: Implement other slots.
+    # - 1: cmd/status addr (check status vs command)
+    # - 2: cmd/status data (check status vs command)
+    # - 3-11: audio out/in
+    # - 12: io control/status (modem GPIO(?))
+
+    def handle_slot(self, slotidx, data_out, data_in):
+        # Process a received slot of a frame.
+        func = self.handle_slots.get(slotidx, self.handle_slot_dummy)
+        bitidx = self.frame_slot_lens[slotidx]
+        bitcount = self.frame_slot_lens[slotidx + 1] - bitidx
+        if data_out is not None:
+            func(slotidx, bitidx, bitcount, True, data_out)
+        if data_in is not None:
+            func(slotidx, bitidx, bitcount, False, data_in)
+
+    def handle_bits(self, ss, es, bit_out, bit_in):
+        # Process a received pair of bits.
+        # Emit the bits' annotations. Only interpret the data when we
+        # are in a frame (have seen the start of the frame, and don't
+        # exceed the expected number of bits in a frame).
+        if bit_out is not None:
+            self.putx(ss, es, Ann.BITS_OUT, ['{:d}'.format(bit_out)])
+        if bit_in is not None:
+            self.putx(ss, es, Ann.BITS_IN, ['{:d}'.format(bit_in)])
+        if self.frame_ss_list is None:
+            return
+        self.frame_ss_list.append(es)
+        have_len = len(self.frame_ss_list) - 1
+        if have_len > self.frame_total_bits:
+            return
+
+        # Accumulate the bits within the frame, until one slot of the
+        # frame has become available.
+        slot_idx = 0
+        if bit_out is not None:
+            self.frame_bits_out.append(bit_out)
+            slot_idx = len(self.frame_slot_data_out)
+        if bit_in is not None:
+            self.frame_bits_in.append(bit_in)
+            slot_idx = len(self.frame_slot_data_in)
+        want_len = self.frame_slot_lens[slot_idx + 1]
+        if have_len != want_len:
+            return
+        prev_len = self.frame_slot_lens[slot_idx]
+
+        # Convert bits to integer values. This shall simplify extraction
+        # of bit fields in multiple other locations.
+        slot_data_out = None
+        if bit_out is not None:
+            slot_bits = self.frame_bits_out[prev_len:]
+            slot_data = self.bits_to_int(slot_bits)
+            self.frame_slot_data_out.append(slot_data)
+            slot_data_out = slot_data
+        slot_data_in = None
+        if bit_in is not None:
+            slot_bits = self.frame_bits_in[prev_len:]
+            slot_data = self.bits_to_int(slot_bits)
+            self.frame_slot_data_in.append(slot_data)
+            slot_data_in = slot_data
+
+        # Emit simple annotations for the integer values, until upper
+        # layer decode stages will be implemented.
+        slot_len = have_len - prev_len
+        slot_ss = self.frame_ss_list[prev_len]
+        slot_es = self.frame_ss_list[have_len]
+        if slot_data_out is not None:
+            slot_text = self.int_to_nibble_text(slot_data_out, slot_len)
+            self.putx(slot_ss, slot_es, Ann.SLOT_OUT_RAW, [slot_text])
+        if slot_data_in is not None:
+            slot_text = self.int_to_nibble_text(slot_data_in, slot_len)
+            self.putx(slot_ss, slot_es, Ann.SLOT_IN_RAW, [slot_text])
+
+        self.handle_slot(slot_idx, slot_data_out, slot_data_in)
+
+    def decode(self):
+        have_sdo = self.has_channel(Pins.SDATA_OUT)
+        have_sdi = self.has_channel(Pins.SDATA_IN)
+        if not have_sdo and not have_sdi:
+            raise ChannelError('Either SDATA_OUT or SDATA_IN (or both) are required.')
+        have_reset = self.has_channel(Pins.RESET)
+
+        # Data is sampled at falling CLK edges. Annotations need to span
+        # the period between rising edges. SYNC rises one cycle _before_
+        # the start of a frame. Grab the earliest SYNC sample we can get
+        # and advance to the start of a bit time. Then keep getting the
+        # samples and the end of all subsequent bit times.
+        prev_sync = [None, None, None]
+        pins = self.wait({Pins.BIT_CLK: 'e'})
+        if pins[Pins.BIT_CLK] == 0:
+            prev_sync[-1] = pins[Pins.SYNC]
+            pins = self.wait({Pins.BIT_CLK: 'r'})
+        bit_ss = self.samplenum
+        while True:
+            pins = self.wait({Pins.BIT_CLK: 'f'})
+            prev_sync.pop(0)
+            prev_sync.append(pins[Pins.SYNC])
+            self.wait({Pins.BIT_CLK: 'r'})
+            if prev_sync[0] == 0 and prev_sync[1] == 1:
+                self.start_frame(bit_ss)
+            self.handle_bits(bit_ss, self.samplenum,
+                    pins[Pins.SDATA_OUT] if have_sdo else None,
+                    pins[Pins.SDATA_IN] if have_sdi else None)
+            bit_ss = self.samplenum