tools: remote: add usb communications protocol
authorSean Cross <sean@xobs.io>
Fri, 19 Apr 2019 14:54:48 +0000 (15:54 +0100)
committerSean Cross <sean@xobs.io>
Fri, 19 Apr 2019 16:29:50 +0000 (17:29 +0100)
This adds a USB communications protocol to the suite of litex-supported
wishbone bridge protocols.

Signed-off-by: Sean Cross <sean@xobs.io>
litex/soc/tools/remote/__init__.py
litex/soc/tools/remote/comm_usb.py [new file with mode: 0644]

index 08b691e9991197300b0cfae4b056ffd7c294dd7c..d61f3888b2dcd8280e94d9302dd369be2b75d13b 100644 (file)
@@ -1,4 +1,5 @@
 from litex.soc.tools.remote.comm_uart import CommUART
 from litex.soc.tools.remote.comm_udp import CommUDP
 from litex.soc.tools.remote.comm_pcie import CommPCIe
+from litex.soc.tools.remote.comm_usb import CommUSB
 from litex.soc.tools.remote.litex_client import RemoteClient
diff --git a/litex/soc/tools/remote/comm_usb.py b/litex/soc/tools/remote/comm_usb.py
new file mode 100644 (file)
index 0000000..1e4ea1e
--- /dev/null
@@ -0,0 +1,142 @@
+import usb.core
+import time
+
+# Wishbone USB Protocol Bridge
+# ============================
+#
+# This module implements a bridge to connect LiteX to the target system's
+# Wishbone bus via USB.  It uses `vendor` packets to communicate, which are
+# normally reserved.  Since we're the vendors of this USB protocol, we take
+# advantage of this packet type to implement the bridge.
+#
+# All traffic goes to/from USB EP0, which is guaranteed to exist regardless
+# of the user's device implementation.  The 8th bit of the first SETUP DATA
+# transaction contains all the information we need to mark this as a
+# Wishbone packet.
+#
+# Packets going to EP0 always start with a SETUP packet, followed by an IN
+# or an OUT packet, followed by an OUT or an IN packet.
+#
+# The SETUP packet looks like this:
+#
+# +----+----+----------+----+----+
+# | C0 | 00 | ADDRESS  | 04 | 00 |   read packet
+# +----+----+----------+----+----+
+#   1    1        4      1    1
+#
+# +----+----+----------+----+----+
+# | 40 | 00 | ADDRESS  | 04 | 00 |   write packet
+# +----+----+----------+----+----+
+#   1    1        4      1    1
+#
+# If the transaction is a "read" transaction, the device responds with an OUT
+# packet with the data.  If the transaction is a "write" transaction, the host
+# responds with an IN packet with the data.
+#
+# Much like other Wishbone bridges, there are two types of packets.  The first
+# byte indicates what type of packet it is, and that it is a Wishbone Bridge
+# packet.  This is the value "0x40" (VENDOR type packet destined for DEVICE)
+# with the "Data Phase Transfer" bit either set or cleared:
+#     - Read:  0xc0
+#     - Write: 0x40
+#
+# The next byte is bRequest, which in the current implementation is unused.
+# Set this value to 0.
+#
+# The next four bytes form the wValue and wIndex values of the SETUP token.
+# We reuse these two 16-bit values as a single 32-bit ADDRESS packet.  Note that
+# USB is big endian.
+#
+# Finally, the last two bytes indicate the length of the transaction.  Since
+# we only support 32-bit reads and writes, this is always 4.  On big endian
+# USB, this has the value {04, 00}.
+
+class CommUSB:
+    def __init__(self, vid=None, pid=None, max_retries=10, debug=False):
+        self.vid = vid
+        self.pid = pid
+        self.debug = debug
+        self.max_retries = max_retries
+        self.MAX_RECURSION_COUNT = 5
+
+    def open(self):
+        if hasattr(self, "dev"):
+            return
+        for t in range(self.max_retries):
+            args = {}
+            if self.vid is not None:
+                args['idVendor'] = self.vid
+            if self.pid is not None:
+                args['idProduct'] = self.pid
+            self.dev = usb.core.find(**args)
+            if self.dev is not None:
+                if self.debug:
+                    print("device connected after {} tries".format(t+1))
+                return True
+            del self.dev
+            time.sleep(0.2 * t)
+        print("unable to find usb device after {} tries".format(self.max_retries))
+        return False
+
+
+    def close(self):
+        if not hasattr(self, "dev"):
+            return
+        del self.dev
+
+    def read(self, addr, length=None):
+        data = []
+        length_int = 1 if length is None else length
+        for i in range(length_int):
+            value = self.usb_read(addr)
+            # Note that sometimes, the value ends up as None when the device
+            # disconnects during a transaction.  Paper over this fact by
+            # replacing it with a sentinal.
+            if value is None:
+                value = 0xffffffff
+            if self.debug:
+                print("read {:08x} @ {:08x}".format(value, addr))
+            if length is None:
+                return value
+            data.append(value)
+        return data
+
+    def usb_read(self, addr, depth=0):
+        try:
+            value = self.dev.ctrl_transfer(bmRequestType=0xc0,
+                        bRequest=0x00,
+                        wValue=addr & 0xffff,
+                        wIndex=(addr >> 16) & 0xffff,
+                        data_or_wLength=4)
+            if value is None:
+                raise TypeError
+            return int.from_bytes(value, byteorder="little")
+        except (usb.core.USBError, TypeError):
+            self.close()
+            self.open()
+            if depth < self.MAX_RECURSION_COUNT:
+                return self.usb_read(addr, depth+1)
+
+    def write(self, addr, data):
+        data = data if isinstance(data, list) else [data]
+        length = len(data)
+        for i, value in enumerate(data):
+            self.usb_write(addr, value)
+            if self.debug:
+                print("write {:08x} @ {:08x}".format(value, addr + 4*i))
+
+    def usb_write(self, addr, value, depth=0):
+        try:
+            self.dev.ctrl_transfer(bmRequestType=0x40, bRequest=0x00,
+                    wValue=addr & 0xffff,
+                    wIndex=(addr >> 16) & 0xffff,
+                    data_or_wLength=bytes([(value >> 0) & 0xff,
+                                           (value >> 8) & 0xff,
+                                           (value >> 16) & 0xff,
+                                           (value >> 24) & 0xff]
+                                          ), timeout=None)
+        except usb.core.USBError:
+            self.close()
+            self.open()
+            if depth < self.MAX_RECURSION_COUNT:
+                return self.usb_write(addr, value, depth+1)