Add libftdi-based SPI class.
authorDan White <dan@whiteaudio.com>
Fri, 13 Jul 2012 01:28:20 +0000 (20:28 -0500)
committerDan White <dan@whiteaudio.com>
Fri, 13 Jul 2012 01:28:20 +0000 (20:28 -0500)
python-lib/usbio.py

index 96f9b86dffa18e4feb775bbbf9235eb3ec24bf31..77833f7ecae9603ec46834c10036ce144eeffa80 100644 (file)
@@ -5,6 +5,7 @@ from time import sleep
 
 import ftdi
 import mpsse
+import struct
 from math import log
 
 from myhdl import intbv
@@ -286,6 +287,314 @@ class I2C(object):
 
 
 
+def uint16str(v):
+    """Return the FTDI style little-endian string of an int."""
+    return struct.pack('<H', v)
+
+
+
+class SPI(object):
+    # Known (possible) issues:
+    #   -calculations for GPIO pins imply 8-bit values
+
+    # shifting command setup for spi modes    edge for: read, write
+    MPSSE_SPI_MODE_CONFIG = (
+            0x00                | ftdi.MPSSE_WRITE_NEG, #rise, fall
+            ftdi.MPSSE_READ_NEG | 0x00                , #fall, rise
+            ftdi.MPSSE_READ_NEG | 0x00                , #fall, rise
+            0x00                | ftdi.MPSSE_WRITE_NEG, #rise, fall
+            )
+    MPSSE_WRITE = ftdi.MPSSE_DO_WRITE
+    MPSSE_READ = ftdi.MPSSE_DO_READ
+
+    # weird mode bits, here for those "non-standard" days...
+    MPSSE_BITWISE_MODE = ftdi.MPSSE_BITMODE
+    MPSSE_LSB_FIRST = ftdi.MPSSE_LSB
+
+    def __init__(self, interface=ftdi.INTERFACE_A, csidle=0, cs=None,
+                    mode=0,
+                    freq=1e6,
+                    vid=0x0403, pid=0x6011,
+                    pindir=0xc0, pinstate=0xc0,
+                    latency=1,
+                    delay=0.001, timeout=100):
+        """SPI port using the MPSSE on compatible FTDI devices with flexible
+        chip-select and composite command sequences.  Uses the libftdi python
+        bindings.
+
+        Target application uses a CS decoder between FT4232H and SPI-controlled
+        chips, no other python libs can deal with this without deep mods.
+        Hence this class..
+
+        interface = ftdi.INTERFACE_x or [1,2,3,4]
+        csidle = 8-bit port state for no chip-selects asserted
+        cs = dict specifying pin states to affect chip-select lines
+            special keys are '_idle': no-cs-asserted pin state
+                             '_mask': bitmask with 1's in CS-related pin bits
+            other keys specify the pin states to assert that device's CS pin
+        mode = SPI mode [0,1,2,3]; bitfield of [CPOL, CPHA]
+        pindir = initial bitfield of pin directions
+        pinstate = initial pin state
+        """
+        self.interface = interface
+        self.csidle = csidle
+        self.cs = cs
+        #defer mode until chip is init'd
+        #defer freq until chip is init'd
+        self.vid = vid
+        self.pid = pid
+        #self._pindir = pindir 
+        #self._pinstate = pinstate
+
+        self.DELAY = delay
+        self.TIMEOUT = timeout
+
+        # setup the driver for this interface, init usb
+        self.context = ftdi.new()
+        ftdi.set_interface(self.context, interface)
+
+        # TODO: what should chunksize be? 4k is libftdi default,
+        # 4232h uses 2k FIFOs
+        # 2232h uses 4k FIFOs
+        #ftdi.write_data_set_chunksize(self.context, 4096)
+        #ftdi.read_data_set_chunksize(self.context, 4096)
+
+        # libftdi has several ways of selecting the desired device, see its
+        # source
+        e = ftdi.usb_open(self.context, vid, pid)
+        if e != 0:
+            raise Exception, ftdi.get_error_string(self.context)
+
+        ftdi.set_bitmode(self.context, int(self.pindir), ftdi.BITMODE_RESET)
+        ftdi.set_bitmode(self.context, int(self.pindir), ftdi.BITMODE_MPSSE)
+        self.set_mode(mode)
+        self.set_freq(freq)
+        self.set_pins(pinstate, pindir)
+        ftdi.usb_purge_buffers(self.context)
+
+    def _raw_write(self, data):
+        """Write data as
+        """
+        #translate data to a string
+        if not isinstance(data, str):
+            data = ''.join(map(chr, data))
+        ret = ftdi.write_data(self.context, data, len(data))
+        if ret != 0:
+            raise Exception, ftdi.get_error_string(self.context)
+
+    def _raw_read(self, n):
+        """Read n bytes from buffer."""
+        nread = 0
+        r = ''
+        while len(r) < n:
+            s = ftdi.read_data(self.context, n)
+            #does this really catch errors??
+            if not s:
+                raise Exception, ftdi.get_error_string(self.context)
+            r += s
+        ftdi.usb_purge_rx_buffer(self.context)
+        return r
+
+    def set_freq(self, freq):
+        """Setup interface to the requested clock frequency.  Calculates the
+        closest divisor settings and returns the actual frequency setting.
+        """
+        # calc rates for both x5 and /5 modes and choose the closest, prefer
+        # the /5 mode if a tie
+        f = CLK / ((1+divisor)*2)
+        options = [] # (prescale, divisor, actual)
+        CLK = 12000000
+        div5 = max(int(round((CLK / (2.0 * freq)))) - 1, 0) #ensure >= 0
+        div1 = max(int(round((5*CLK / (2.0 * freq)))) - 1, 0)
+        act5 = CLK / (2.0 * (1 + div5))
+        act1 = 5*CLK / (2.0 * (1 + div5))
+
+        if abs(freq - act5) <= abs(freq - act1):
+            div = div5
+            act = act5
+            #reset state has 60 MHz / 5 prescaler enabled
+            #ftdi.write_data(self.context, chr(ftdi.EN_DIV_5), 1)
+        else:
+            div = div1
+            act = act1
+            ftdi.write_data(self.context, chr(ftdi.DIS_DIV_5), 1)
+
+        cmd = chr(ftdi.TCK_DIVISOR) + uint16str(div)
+        ftdi.write_data(self.context, cmd, len(cmd))
+        self._freq = act
+        return act
+
+    @property
+    def freq(self):
+        """Actual clock frequency from the current chip settings.  See
+        set_freq() to change.
+        """
+        return self._freq
+
+    def set_mode(self, mode):
+        if mode in range(4):
+            #ensure pin 0/SK starts in its idle state in case this is a mode change
+            #yes, the check is done in set_pins(), but paranoia never hurts..
+            p = self._pinstate
+            if self.mode in (0, 1):
+                #cpol = 0
+                p &= 0xFE
+            else:
+                #cpol = 1
+                p |= 0x01
+            self.set_pins(p)
+            self._mode = mode
+        else:
+            raise Exception, 'Invalid mode %i, must be in range(4)' % mode
+
+    @property
+    def mode(self):
+        """Return the current SPI mode.  See set_mode() to change it.
+        """
+        return self._mode
+
+    def set_pins(pinstate=None, pindir=None, audit_cs=True):
+        """Change the GPIO pins [3..7] to the given state and direction.
+        Argument values of None mean "do not change".  Audits the values to
+        keep the SPI SK/DO/DI pins and configured chip-select pins in correct
+        states (idle mode).  If audit_cs==False, allows setting the output
+        state of the configured chip-select pins.
+        """
+        if pindir is not None:
+            self._pindir = pindir
+            #we are changing pin directions, audit them
+            self._pindir |= 0x03 # pins 0/SK and 1/DO are outputs for sure
+            self._pindir |= self._cs_mask # chip-select pins are always outputs
+            self._pindir &= 0xFB # pin 2/DI is an input for sure
+            # most implementations force pin 3/CS to an output, IT DOES NOT
+            # NEED DO BE.  The top 5 pins are available for GPIO, CS, or both
+            # in SPI mode.  With a 5-bit one-cold decoder, we can control up to
+            # (2**5 - 1) = 31 chips with this port.
+
+        if pinstate is not None:
+            if audit_cs:
+                self._pinstate = ((pinstate & ~self._cs_mask) +
+                                    (self.csidle & self._cs_mask))
+            else:
+                self._pinstate = pinstate
+
+            #ensure pin 0/SK stays in its idle state, 1/DO stays as-was
+            if self.mode in (0, 1):
+                #cpol = 0
+                self._pinstate &= 0xFE
+            else:
+                #cpol = 1
+                self._pinstate |= 0x01
+
+        cmd = chr(ftdi.SET_BITS_LOW) + chr(self._pinstate) + chr(self._pindir)
+        self._raw_write(cmd)
+
+    def get_pins(self):
+        cmd = chr(ftdi.GET_BITS_LOW) + chr(ftdi.SEND_IMMEDIATE)
+        self._raw_write(cmd)
+        p = self._read_raw(1)
+        self._pinstate = p
+        return p
+
+    @property
+    def pinstate(self):
+        """Returns the last-known pin state.  Get a fresh version with
+        get_pins().
+        """
+        return self._pinstate
+
+    @property
+    def pindir(self):
+        """Returns the last-set pin direction bitfield."""
+        return self._pindir
+
+    def _cs_cmd(self, device):
+        """Construct a pin-setting command which asserts the given device.  See
+        __init__() for more info.
+        """
+        mask = self.cs['_mask']
+        p = self._pinstate & ~mask
+        p += self.cs[device] & mask
+
+        self._pinstate = p
+        cmd = chr(ftdi.SET_BITS_LOW) + chr(p) + chr(self._pindir)
+        return cmd
+
+    def _write_cmd(self, data):
+        """Return a MPSSE command string which sends data out via the current SPI
+        mode.  data is either a string or converted by map(chr, data).
+        """
+        cmd = chr(self.MPSSE_SPI_MODE_CONFIG[self.mode] |
+                  self.MPSSE_WRITE)
+        cmd += uint16str(len(data) - 1)
+
+        #translate data to a string
+        if not isinstance(data, str):
+            data = ''.join(map(chr, data))
+
+        cmd += data
+        return cmd
+
+    def _exchange_cmd(self, data):
+        """Return a MPSSE command string which sends data out via the current SPI
+        mode and reads the same amount back.  data is either a string or
+        converted by map(chr, data).  Interface read buffer contains the read
+        data.
+        """
+        cmd = chr(self.MPSSE_SPI_MODE_CONFIG[self.mode] |
+                  self.MPSSE_WRITE |
+                  self.MPSSE_READ)
+        cmd += uint16str(len(data) - 1)
+
+        #translate data to a string
+        if not isinstance(data, str):
+            data = ''.join(map(chr, data))
+
+        cmd += data
+        cmd += chr(ftdi.SEND_IMMEDIATE)
+        return cmd
+
+    def _read_cmd(self, n):
+        """Return a MPSSE command string which reads n-bytes via the current
+        SPI mode.  NB: The DO line stays in its last state.
+        """
+        cmd = chr(self.MPSSE_SPI_MODE_CONFIG[self.mode] |
+                  self.MPSSE_READ)
+        cmd += uint16str(n - 1)
+        cmd += chr(ftdi.SEND_IMMEDIATE)
+        return cmd
+
+    def write(self, data, device):
+        """Write data out to the selected device via the current SPI mode.
+        The device is used to select the desired CS pin state as cs_active =
+        self.cs[device].
+        """
+        cmd = (self._cs_cmd(device) + self._write_cmd(data) +
+                self._cs_cmd('_idle'))
+        self._raw_write(cmd)
+
+    def read(self, n, device):
+        """Read data from the selected device, DO pin stays in its last
+        state.
+        """
+        cmd = (self._cs_cmd(device) + self._read_cmd(n) +
+                self._cs_cmd('_idle'))
+        self._raw_write(cmd)
+        return self._raw_read(n)
+
+    def exchange(self, data, device):
+        """Write data out to the selected device while also reading data in via
+        the current SPI mode.  The device is used to select the desired CS pin
+        state as cs_active = self.cs[device].
+        """
+        cmd = (self._cs_cmd(device) + self._exchange_cmd(data) +
+                self._cs_cmd('_idle'))
+        self._raw_write(cmd)
+        return self._raw_read(n)
+
+
+
+
 class NCO(object):
     RST_POS = 14
     FCW_WIDTH = 14