usbi2c.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. """Host library for the ATmega8U2 USB-I2C bridge + TCA9539 helper. pip install pyserial"""
  2. import struct
  3. import serial
  4. MAGIC = 0xA5
  5. OP_XFER, OP_SPEED, OP_PING, OP_RECOVER = 1, 2, 3, 4
  6. MAXLEN = 32
  7. ERRORS = {1: "NACK on address", 2: "NACK on data", 3: "bus error / timeout",
  8. 0x10: "unknown op", 0x11: "bad length"}
  9. class I2CError(IOError):
  10. def __init__(self, status):
  11. super().__init__(f"I2C error {status}: {ERRORS.get(status, '?')}")
  12. self.status = status
  13. class Bridge:
  14. def __init__(self, port, timeout=0.5):
  15. self.s = serial.Serial(port, 115200, timeout=timeout) # baud is ignored by CDC
  16. self.s.reset_input_buffer()
  17. self.ping()
  18. def close(self):
  19. self.s.close()
  20. def _read(self, n):
  21. d = self.s.read(n)
  22. if len(d) != n:
  23. raise TimeoutError("no reply from bridge")
  24. return d
  25. def ping(self):
  26. self.s.write(bytes([MAGIC, OP_PING]))
  27. st, proto, maxlen = self._read(3)
  28. if st:
  29. raise I2CError(st)
  30. return proto, maxlen
  31. def set_delay(self, n):
  32. """SCL delay; 0 = 400 kHz (default), each step adds 0.375 us per period."""
  33. self.s.write(bytes([MAGIC, OP_SPEED, n]))
  34. st, = self._read(1)
  35. if st:
  36. raise I2CError(st)
  37. def recover(self):
  38. self.s.write(bytes([MAGIC, OP_RECOVER]))
  39. st, = self._read(1)
  40. if st:
  41. raise I2CError(st)
  42. @staticmethod
  43. def _frame(addr, w, rlen):
  44. w = bytes(w)
  45. if len(w) > MAXLEN or rlen > MAXLEN:
  46. raise ValueError("max 32 bytes per phase")
  47. return bytes([MAGIC, OP_XFER, addr, len(w), rlen]) + w
  48. def xfer(self, addr, w=b"", rlen=0):
  49. """One transaction: write w, then (repeated START) read rlen bytes."""
  50. return self.batch([(addr, w, rlen)])[0]
  51. def batch(self, ops, raise_on_error=True):
  52. """Pipeline many (addr, wdata, rlen) transactions in as few USB packets as possible.
  53. This is the fast path: one USB round trip (~1 ms) for dozens of transactions."""
  54. ops = list(ops)
  55. self.s.write(b"".join(self._frame(*o) for o in ops))
  56. out, first_err = [], 0
  57. for _, _, rlen in ops: # always drain every reply to stay in sync
  58. st, = self._read(1)
  59. if st:
  60. first_err = first_err or st
  61. out.append(None)
  62. else:
  63. out.append(self._read(rlen) if rlen else b"")
  64. if first_err and raise_on_error:
  65. raise I2CError(first_err)
  66. return out
  67. def scan(self):
  68. res = self.batch([(a, b"", 0) for a in range(8, 120)], raise_on_error=False)
  69. return [a for a, r in zip(range(8, 120), res) if r is not None]
  70. class TCA9539:
  71. IN0, OUT0, POL0, CFG0 = 0x00, 0x02, 0x04, 0x06
  72. def __init__(self, bridge, addr=0x74): # 0x74..0x77 from A1/A0
  73. self.b, self.addr = bridge, addr
  74. def _w16(self, reg, v):
  75. self.b.xfer(self.addr, [reg, v & 0xFF, v >> 8])
  76. def _r16(self, reg):
  77. lo, hi = self.b.xfer(self.addr, [reg], 2)
  78. return lo | hi << 8
  79. def set_direction(self, input_mask): # bit=1 -> input (power-on default 0xFFFF)
  80. self._w16(self.CFG0, input_mask)
  81. def write(self, value):
  82. self._w16(self.OUT0, value)
  83. def read(self):
  84. return self._r16(self.IN0)
  85. def set_polarity(self, mask):
  86. self._w16(self.POL0, mask)
  87. if __name__ == "__main__":
  88. import sys, time
  89. b = Bridge(sys.argv[1])
  90. print("proto/maxlen:", b.ping())
  91. found = b.scan()
  92. print("devices:", [hex(a) for a in found])
  93. if found:
  94. io = TCA9539(b, found[0])
  95. io.set_direction(0xFF00) # port0 outputs, port1 inputs
  96. for i in range(8):
  97. io.write(1 << i)
  98. time.sleep(0.1)
  99. io.write(0)
  100. print("port1 inputs: %02X" % (io.read() >> 8))
  101. t = time.time()
  102. for _ in range(200):
  103. io.write(0x55) # unbatched: bound by USB 1 ms polling
  104. print("single writes/s: %.0f" % (200 / (time.time() - t)))
  105. t = time.time()
  106. b.batch([(found[0], [TCA9539.OUT0, 0x55, 0], 0)] * 400)
  107. print("batched writes/s: %.0f" % (400 / (time.time() - t)))