I2C: TPM: Add tools for analyzing i2c and tpm transactions

The Saleae Logic USB logic analyzer can dump I2C transactions
to CSV files.  These scripts can take those I2C transactions and
extract TPM transactions for debugging.  The dump_i2c script can
be reused for analyzing other device protocols as well.

BUG=None
TEST=run 'dump_i2c --test' and 'dump_tpm --test'

Change-Id: Id7018d14497dfb9ac6cc7120a1092e70ea1ef725
Reviewed-on: http://gerrit.chromium.org/gerrit/5611
Reviewed-by: Anton Staaf <[email protected]>
Tested-by: Anton Staaf <[email protected]>
diff --git a/host/dump_i2c b/host/dump_i2c
new file mode 100755
index 0000000..7e7c7dd
--- /dev/null
+++ b/host/dump_i2c
@@ -0,0 +1,312 @@
+#!/usr/bin/env python
+# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Dump I2C transactions from raw CSV record produced by Saleae Logic.
+
+The input file format for I2C transaction data is a comma separated value
+(CSV) file with a single header row.  The columns of the file that are
+used by dump_i2c are in the following format (data is optional for some
+events.
+
+<time>, <event>, [data]
+
+Additional columns after the first three and the header row are ignored.
+"""
+
+import fileinput
+import optparse
+import os
+import re
+import sys
+
+class I2CParseError(Exception):
+  """I2C parsing error exception.
+
+  This exception is raised when the parser failes to understand the input
+  provided.  It records the file name and line number for later formatting
+  of an error message that includes a message that is specific to the parse
+  error.
+  """
+  def __init__(self, message):
+    """Initialize a new I2CParseError exception."""
+    self.message = message
+
+    try:
+      self.file_name = fileinput.filename()
+      self.line_number = fileinput.filelineno()
+    except RuntimeError:
+      self.file_name = '<doctest>'
+      self.line_number = 0
+
+  def __str__(self):
+    """Format the exception for user consumption."""
+    return '%s:%d: %s' % (self.file_name, self.line_number, self.message)
+
+
+class Transition:
+  """Structure describing entries in the state transition table."""
+  def __init__(self, current_state, event, next_state, action):
+    self.current_state = current_state
+    self.event = event
+    self.next_state = next_state
+    self.action = action
+
+
+class I2C:
+  """State machine class that accumulates and prints full I2C bus transactions.
+
+  Once a complete bus transaction is encountered it is printed.  The output of
+  this class can be further processed by device specific scripts to further
+  understand the transaction.
+
+  This example shows the basic functionality of I2C.
+  >>> i2c = I2C(0x20, 1)
+  >>> i2c.process('0.1,Start Bit,')
+  >>> i2c.process('0.2,Write Address + ACK, 0x20')
+  >>> i2c.process('0.3,Data + ACK, 0x00')
+  >>> i2c.process('0.4,Stop Bit,')
+  0.10000000 Write 0x20 DATA 0x00 
+
+  Here we see I2C filtering out a transaction based on the device address.
+  >>> i2c = I2C(0x20, 1)
+  >>> i2c.process('0.1,Start Bit,')
+  >>> i2c.process('0.2,Write Address + ACK, 0xff')
+  >>> i2c.process('0.3,Data + ACK, 0x00')
+  >>> i2c.process('0.4,Stop Bit,')
+
+  Here is an example of an invalid I2C transaction sequence, there can not be
+  two start bits in a row.
+  >>> i2c = I2C(0x20, 1)
+  >>> i2c.process('0.1,Start Bit,')
+  >>> i2c.process('0.1,Start Bit,')
+  Traceback (most recent call last):
+  ...
+  I2CParseError: <doctest>:0: Unexpected event "Start Bit"
+
+  This is an example of I2C syncing to the beginning of the first full
+  transaction presented to it.
+  >>> i2c = I2C(0x20, 1)
+  >>> i2c.process('0.1,Stop Bit,')
+  >>> i2c.process('0.1,Start Bit,')
+  >>> i2c.state == i2c.STARTED
+  True
+
+  And a completely bogus value results in a ValueError when trying to convert
+  the time string to a float.
+  >>> i2c = I2C(0x20, 1)
+  >>> i2c.process('this,is,not,valid')
+  Traceback (most recent call last):
+  ...
+  ValueError: invalid literal for float(): this
+
+  Or a truncated line will throw an IndexError
+  >>> i2c = I2C(0x20, 1)
+  >>> i2c.process('0.1')
+  Traceback (most recent call last):
+  ...
+  IndexError: list index out of range
+  """
+
+  SYNC = 0
+  IDLE = 1
+  STARTED = 2
+  READING = 3
+  WRITING = 4
+  NAK = 5
+
+  def StartBit(self, time, data):
+    """Record start time of transaction."""
+    self.message += '%.8f ' % time
+
+  def WriteAddressNAK(self, time, data):
+    """Record NAK'ed address transaction for writing."""
+    self.address = int(data, 16)
+    self.message += 'Write %s NAK' % data
+
+  def WriteAddressACK(self, time, data):
+    """Record ACK'ed address transaction for writing."""
+    self.address = int(data, 16)
+    self.message += 'Write %s DATA ' % data
+
+  def ReadAddressNAK(self, time, data):
+    """Record NAK'ed address transaction for reading."""
+    self.address = int(data, 16)
+    self.message += 'Read  %s NAK' % data
+
+  def ReadAddressACK(self, time, data):
+    """Record ACK'ed address transaction for reading."""
+    self.address = int(data, 16)
+    self.message += 'Read  %s DATA ' % data
+
+  def AddData(self, time, data):
+    """Record read or written data."""
+    self.message += '%s ' % data
+
+  def ClearMessage(self, time, data):
+    """Clear accumulated transaction."""
+    self.message = ''
+
+  def PrintMessage(self, time, data):
+    """Print and clear accumulated transaction."""
+    if self.address == self.match_address:
+      print self.message
+
+    self.message = ''
+
+  # This state transition table records the valid I2C bus transitions that we
+  # expect to see.  Any state/action pair not defined in this table is assumed
+  # to be invalid and will result in an I2CParseError being raised.
+  #
+  # The entries in this table correspond to the current state, the event
+  # parsed, the state to transition to and the function to execute on that
+  # transition.  The function is passed a CSV instance, the time of the event
+  # and a possibly empty data field.
+  state_table = [
+    # The initial section of the state transition table describes the
+    # synchronization process.  For the I2C bus this means waiting for
+    # the first start or repeated start bit.  We can also transition to
+    # the IDLE state when we see a stop bit because the next bit has to be
+    # a start bit.  If it's not we'll raise a I2CParseError exception.
+    Transition(SYNC,    'Start Bit',           STARTED, StartBit),
+    Transition(SYNC,    'Repeated Start Bit',  STARTED, StartBit),
+    Transition(SYNC,    'Write Address + NAK', SYNC,    None),
+    Transition(SYNC,    'Write Address + ACK', SYNC,    None),
+    Transition(SYNC,    'Read Address + NAK',  SYNC,    None),
+    Transition(SYNC,    'Read Address + ACK',  SYNC,    None),
+    Transition(SYNC,    'Data + NAK',          SYNC,    None),
+    Transition(SYNC,    'Data + ACK',          SYNC,    None),
+    Transition(SYNC,    'Stop Bit',            IDLE,    None),
+
+    # After syncronization is complete the rest of the table describes the
+    # expected transitions.
+    Transition(IDLE,    'Start Bit',           STARTED, StartBit),
+    Transition(STARTED, 'Stop Bit',            IDLE,    ClearMessage),
+    Transition(STARTED, 'Write Address + NAK', NAK,     WriteAddressNAK),
+    Transition(STARTED, 'Write Address + ACK', WRITING, WriteAddressACK),
+    Transition(STARTED, 'Read Address + NAK',  NAK,     ReadAddressNAK),
+    Transition(STARTED, 'Read Address + ACK',  READING, ReadAddressACK),
+    Transition(WRITING, 'Data + NAK',          NAK,     AddData),
+    Transition(WRITING, 'Data + ACK',          WRITING, AddData),
+    Transition(READING, 'Data + NAK',          NAK,     AddData),
+    Transition(READING, 'Data + ACK',          READING, AddData),
+    Transition(WRITING, 'Stop Bit',            IDLE,    PrintMessage),
+    Transition(WRITING, 'Repeated Start Bit',  STARTED, PrintMessage),
+    Transition(NAK,     'Stop Bit',            IDLE,    PrintMessage)]
+
+  def __init__(self, match_address, timeout):
+    """Initialize a new I2C instance.
+
+    The I2C instance will print all transactions with a particular I2C device
+    specified by it's address up until the timeout.
+
+    Args:
+      match_address: I2C device address to filter for
+      timeout: Maximum time to start recording new transactions
+
+    >>> i2c = I2C(0x20, 1)
+    >>> i2c.match_address == 0x20
+    True
+    >>> i2c.timeout == 1
+    True
+    >>> i2c.state == i2c.SYNC
+    True
+    """
+    self.state = self.SYNC
+    self.address = 0x00
+    self.message = ''
+    self.match_address = match_address
+    self.timeout = timeout
+
+  def process(self, line):
+    """Update I2C state machine from one line of the CSV file.
+
+    The CSV file is assumed to have the format generated by the Saleae Logic
+    desktop I2C recording tool.
+
+    These examples show how process effects the internal state of I2C.
+    >>> i2c = I2C(0x20, 1)
+
+    >>> i2c.process('0.1,Start Bit,')
+    >>> i2c.state == i2c.STARTED
+    True
+    >>> i2c.message == '0.10000000 '
+    True
+
+    >>> i2c.process('0.1,Stop Bit,')
+    >>> i2c.state == i2c.IDLE
+    True
+    >>> i2c.message == ''
+    True
+    """
+    values = line.split(',')
+
+    time = float(values[0])
+    detail = ' '.join(values[1].split())
+
+    if len(values) > 2:
+      data = ' '.join(values[2].split())
+    else:
+      data = ''
+
+    # Once the timeout value has been reached in the input trace we ignore all
+    # future events once we've returned to the IDLE state.  We return to the
+    # IDLE state at the next "Stop Bit" and stay there.
+    if time > self.timeout and self.state == self.IDLE:
+      return
+
+    # Search the transition table for a matching state/action pair.
+    for transition in self.state_table:
+      if (transition.current_state == self.state and
+          transition.event == detail):
+        if transition.action:
+          transition.action(self, time, data)
+
+        self.state = transition.next_state
+        break
+    else:
+      raise I2CParseError('Unexpected event "%s"' % detail)
+
+
+def main():
+  parser = optparse.OptionParser(usage = 'usage: %prog [filename] [options]\n')
+
+  parser.add_option('-a', '--address', default=0x20,
+                    type='int',
+                    help='I2C device address to process',
+                    action='store',
+                    dest='address')
+
+  parser.add_option('-t', '--timeout', default=100,
+                    type='float',
+                    help='All transactions before timeout are shown',
+                    action='store',
+                    dest='timeout')
+
+  options, arguments = parser.parse_args()
+
+  input = fileinput.input(arguments)
+  i2c = I2C(options.address, options.timeout)
+
+  for line in input:
+    # The first line of the file is the header row.
+    if not fileinput.isfirstline():
+      try:
+        i2c.process(line)
+      except (I2CParseError, ValueError, IndexError) as error:
+        print error
+        return
+
+def Test():
+  """Run any built-in tests."""
+  import doctest
+  doctest.testmod()
+
+
+if __name__ == '__main__':
+  # If first argument is --test, run testing code.
+  if sys.argv[1:2] == ['--test']:
+    Test()
+  else:
+    main()
diff --git a/host/dump_tpm b/host/dump_tpm
new file mode 100755
index 0000000..8842e01
--- /dev/null
+++ b/host/dump_tpm
@@ -0,0 +1,246 @@
+#!/usr/bin/env python
+# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Dump tpm transactions from previous run of dump_i2c.
+
+The output of dump_i2c can be used directly by piping it to dump_tpm's stdin.
+The dump_tpm input format is line oriented where each line should conform to:
+
+<time> <Read|Write> <address in 0xff format> NAK
+
+or:
+
+<time> <Read|Write> <address in 0xff format> DATA [data]+
+"""
+
+import fileinput
+import optparse
+import os
+import re
+import sys
+
+class TPMParseError(Exception):
+  """TPM parsing error exception.
+
+  This exception is raised when the parser failes to understand the input
+  provided.  It records the file name and line number for later formatting
+  of an error message that includes a message that is specific to the parse
+  error.
+  """
+  def __init__(self, message):
+    """Initialize a new TPMParseError exception."""
+    self.message = message
+
+    try:
+      self.file_name = fileinput.filename()
+      self.line_number = fileinput.filelineno()
+    except RuntimeError:
+      self.file_name = '<doctest>'
+      self.line_number = 0
+
+  def __str__(self):
+    """Format the exception for user consumption."""
+    return '%s:%d: %s' % (self.file_name, self.line_number, self.message)
+
+
+class Transition:
+  """Structure describing entries in the state transition table."""
+  def __init__(self, current_state, event, next_state, action):
+    self.current_state = current_state
+    self.event = event
+    self.next_state = next_state
+    self.action = action
+
+
+class TPM:
+  """State machine that reads I2C transactions and prints TPM transactions.
+
+  TPM interprets single byte writes as precursors to reads since the first
+  byte in any write is used to set the register address for subsequent bytes
+  in the write or future reads.
+  >>> tpm = TPM()
+  >>> tpm.process('0.1 Write 0x20 DATA 0x00')
+  >>> tpm.process('0.2 Read 0x20 DATA 0x81')
+  Read reg TPM_ACCESS_0 returns 0x81
+
+  A write with multiple bytes is interpreted as a write to the register
+  addressed by the first byte in the sequence.
+  >>> tpm = TPM()
+  >>> tpm.process('0.1 Write 0x20 DATA 0x09 0x12 0x34')
+  Write reg TPM_DID_VID_0.3 with 0x12 0x34
+
+  TPM interprets a write that is not a single byte write that sets the same
+  TPM register address as an error.
+  >>> tpm = TPM()
+  >>> tpm.process('0.1 Write 0x20 DATA 0x08')
+  >>> tpm.process('0.2 Write 0x20 DATA 0x09 0x12')
+  Traceback (most recent call last):
+  ...
+  TPMParseError: <doctest>:0: Unexpected action "0.2 Write 0x20 DATA 0x09 0x12"
+
+  But if the write is a redundant setting of the TPM register address it is OK
+  >>> tpm = TPM()
+  >>> tpm.process('0.1 Write 0x20 DATA 0x08')
+  >>> tpm.process('0.2 Write 0x20 DATA 0x08')
+  """
+  SYNC = 0
+  IDLE = 1
+  READ = 2
+
+  ACTION_WRITE_MULTI = 0
+  ACTION_WRITE_SINGLE = 1
+  ACTION_READ = 2
+
+  def RegisterName(self, index):
+    register = ['TPM_ACCESS_0',
+                'TPM_STS_0',
+                'TPM_STS_0.BC_0',
+                'TPM_STS_0.BC_1',
+                'TPM_STS_0.BC_2',
+                'TPM_DATA_FIFO_0',
+                'TPM_DID_VID_0.0',
+                'TPM_DID_VID_0.1',
+                'TPM_DID_VID_0.2',
+                'TPM_DID_VID_0.3']
+
+    if index < len(register):
+      return register[index]
+    else:
+      raise TPMParseError('Unknown register index %d' % index)
+
+  def PrintWrite(self, data):
+    """Print TPM write transaction."""
+    self.register = int(data[0], 16)
+
+    print 'Write reg %s with %s' % (self.RegisterName(self.register),
+                                    ' '.join(data[1:]))
+
+  def RecordAddress(self, data):
+    """Record TPM register address."""
+    self.register = int(data[0], 16)
+
+  def CheckDuplicateAddress(self, data):
+    """Check that a write while waiting for a read is actually a duplicate."""
+    if self.register != int(data[0], 16):
+      raise TPMParseError('Expected "Read" action, got "Write" to new address')
+
+  def PrintRead(self, data):
+    """Print TPM read transaction."""
+    print 'Read reg %s returns %s' % (self.RegisterName(self.register),
+                                      ' '.join(data))
+
+  # This state transition table records the valid I2C bus actions for
+  # communicating with the TPM.  And state/action pair not defined in this
+  # table is assumed to be invalid and will result in an TPMParseError being
+  # raised.
+  #
+  # The entries in this table correspond to the current state, the event
+  # parsed, the state to transition to and the function to execute on that
+  # transition.  The function is passed a TPM instance and an array of the
+  # data values parsed from the event.
+  state_table = [
+    # The initial section of the state transition table describes the
+    # synchronization process.  For the TPM this just involves waiting for
+    # the first write operation.
+    Transition(SYNC, ACTION_WRITE_MULTI,  IDLE, PrintWrite),
+    Transition(SYNC, ACTION_WRITE_SINGLE, READ, RecordAddress),
+    Transition(SYNC, ACTION_READ,         SYNC, None),
+
+    # After syncronization is complete the rest of the table describes the
+    # expected transitions.
+    Transition(IDLE, ACTION_WRITE_MULTI,  IDLE, PrintWrite),
+    Transition(IDLE, ACTION_WRITE_SINGLE, READ, RecordAddress),
+    Transition(READ, ACTION_WRITE_SINGLE, READ, CheckDuplicateAddress),
+    Transition(READ, ACTION_READ,         IDLE, PrintRead)]
+
+  def __init__(self):
+    """Initialize a new TPM instance.
+
+    >>> tpm = TPM()
+    >>> tpm.state == tpm.SYNC
+    True
+    """
+    self.state = self.SYNC
+    self.register = 0x00
+
+  def process(self, line):
+    """Update TPM state machine for one line generated by dump_i2c.
+
+    These examples show how process effects the internal state of TPM.
+    >>> tpm = TPM()
+
+    >>> tpm.process('0.1 Write 0x20 DATA 0x00')
+    >>> tpm.register == 0x00
+    True
+    >>> tpm.state == tpm.READ
+    True
+
+    >>> tpm.process('0.2 Read 0x20 DATA 0x81')
+    Read reg TPM_ACCESS_0 returns 0x81
+    >>> tpm.state == tpm.IDLE
+    True
+
+    >>> tpm.process('0.10000000 Write 0x20 DATA 0x09 0x12 0x34')
+    Write reg TPM_DID_VID_0.3 with 0x12 0x34
+    >>> tpm.register == 0x09
+    True
+    >>> tpm.state == tpm.IDLE
+    True
+    """
+    values  = line.split()
+    time    = float(values[0])
+    action  = ' '.join(values[1].split())
+    address = ' '.join(values[2].split())
+    nak     = (values[3].split()[0] == 'NAK')
+    data    = values[4:]
+
+    if nak:
+      return
+
+    if action == 'Read':
+      action_index = self.ACTION_READ
+    elif action == 'Write' and len(data) == 1:
+      action_index = self.ACTION_WRITE_SINGLE
+    elif action == 'Write' and len(data) > 1:
+      action_index = self.ACTION_WRITE_MULTI
+    else:
+      raise TPMParseError('Unknown action "%s"' % action)
+
+    # Search the transition table for a matching state/action pair.
+    for transition in self.state_table:
+      if (transition.current_state == self.state and
+          transition.event == action_index):
+        if transition.action:
+          transition.action(self, data)
+
+        self.state = transition.next_state
+        break
+    else:
+      raise TPMParseError('Unexpected action "%s"' % line)
+
+
+def main():
+  tpm = TPM()
+
+  for line in fileinput.input():
+    try:
+      tpm.process(line)
+    except (TPMParseError, ValueError, IndexError) as error:
+      print error
+      return
+
+
+def Test():
+  """Run any built-in tests."""
+  import doctest
+  doctest.testmod()
+
+
+if __name__ == '__main__':
+  # If first argument is --test, run testing code.
+  if sys.argv[1:2] == ['--test']:
+    Test()
+  else:
+    main()