165 lines
5.7 KiB
Python
165 lines
5.7 KiB
Python
"""I2C master over a CH347 USB adapter (Waveshare USB to UART/I2C/SPI/JTAG).
|
|
|
|
Knows nothing about any particular I2C device -- it only moves bytes to and
|
|
from a 7-bit address.
|
|
|
|
The CH347 in Mode 1 (UART1+I2C+SPI) presents three interfaces: two CDC-ACM
|
|
interfaces that the kernel binds for the UART, and interface 2, a vendor-class
|
|
interface carrying I2C/SPI/JTAG that no kernel driver claims. We talk to
|
|
interface 2 directly over libusb.
|
|
|
|
Framing follows the CH347 stream protocol as implemented by the
|
|
aystarik/ch347-i2c-spi-gpio Linux driver, whose USB id table entry
|
|
USB_DEVICE_INTERFACE_NUMBER(0x1a86, 0x55db, 0x02) matches this device.
|
|
"""
|
|
|
|
import usb.core
|
|
import usb.util
|
|
|
|
VENDOR_ID = 0x1A86
|
|
PRODUCT_ID = 0x55DB
|
|
INTERFACE = 2
|
|
|
|
EP_OUT = 0x06
|
|
EP_IN = 0x86
|
|
|
|
TIMEOUT_MS = 1000
|
|
|
|
# Stream protocol opcodes.
|
|
CMD_STREAM = 0xAA # start of an I2C command stream
|
|
CMD_END = 0x00 # end of stream
|
|
CMD_STA = 0x74 # emit START
|
|
CMD_STO = 0x75 # emit STOP
|
|
CMD_OUT = 0x80 # write; low 6 bits are the byte count
|
|
CMD_IN = 0xC0 # read; low 6 bits are the byte count
|
|
CMD_SET = 0x60 # set bus speed; low bits select the rate
|
|
|
|
# Bus speeds, as the low nibble of CMD_SET.
|
|
SPEED_20KHZ = 0
|
|
SPEED_100KHZ = 1
|
|
SPEED_400KHZ = 2
|
|
SPEED_750KHZ = 3
|
|
|
|
# The count field is 6 bits, and a write also spends one byte on the address.
|
|
MAX_XFER = 0x3F
|
|
|
|
ACK = 1
|
|
|
|
|
|
class CH347I2C:
|
|
"""I2C master on a CH347 adapter.
|
|
|
|
Every byte the adapter clocks out produces one status byte in the reply,
|
|
where 1 means the slave ACKed. A read reply is one address-ACK byte
|
|
followed by the payload.
|
|
"""
|
|
|
|
def __init__(self, speed=SPEED_100KHZ):
|
|
self._dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID)
|
|
if self._dev is None:
|
|
raise IOError(
|
|
f"No CH347 adapter found ({VENDOR_ID:04x}:{PRODUCT_ID:04x}). "
|
|
"Is it plugged in and in Mode 1?"
|
|
)
|
|
|
|
# Deliberately no set_configuration(): the device is already configured,
|
|
# and re-setting it would reset the CDC-ACM interfaces the kernel is
|
|
# using for /dev/ttyACM*.
|
|
try:
|
|
usb.util.claim_interface(self._dev, INTERFACE)
|
|
except usb.core.USBError as exc:
|
|
raise IOError(
|
|
f"Cannot claim CH347 interface {INTERFACE}: {exc}. "
|
|
"If this is a permission error, run ./setup.sh."
|
|
) from exc
|
|
|
|
self._claimed = True
|
|
self.set_speed(speed)
|
|
|
|
def close(self):
|
|
if getattr(self, "_claimed", False):
|
|
usb.util.release_interface(self._dev, INTERFACE)
|
|
usb.util.dispose_resources(self._dev)
|
|
self._claimed = False
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *exc_info):
|
|
self.close()
|
|
return False
|
|
|
|
def _xfer(self, out_bytes, in_len):
|
|
"""Send one command stream, then read in_len status/data bytes."""
|
|
written = self._dev.write(EP_OUT, bytes(out_bytes), TIMEOUT_MS)
|
|
if written != len(out_bytes):
|
|
raise IOError(
|
|
f"Short USB write to CH347: sent {written} of {len(out_bytes)} bytes"
|
|
)
|
|
if in_len == 0:
|
|
return b""
|
|
|
|
reply = bytes(self._dev.read(EP_IN, in_len, TIMEOUT_MS))
|
|
if len(reply) != in_len:
|
|
raise IOError(
|
|
f"Short USB read from CH347: got {len(reply)} of {in_len} bytes"
|
|
)
|
|
return reply
|
|
|
|
def set_speed(self, speed):
|
|
"""Select the I2C clock rate (one of the SPEED_* constants)."""
|
|
if speed not in (SPEED_20KHZ, SPEED_100KHZ, SPEED_400KHZ, SPEED_750KHZ):
|
|
raise ValueError(f"Invalid I2C speed {speed}, expected 0-3")
|
|
self._xfer([CMD_STREAM, CMD_SET | speed, CMD_END], 0)
|
|
|
|
def write(self, addr, data):
|
|
"""Write data (bytes) to a 7-bit address. Raises IOError on NACK."""
|
|
data = bytes(data)
|
|
# One byte of the transfer budget goes to the address.
|
|
if len(data) > MAX_XFER - 1:
|
|
raise ValueError(
|
|
f"Write of {len(data)} bytes exceeds the CH347 limit of {MAX_XFER - 1}"
|
|
)
|
|
|
|
packet = [CMD_STREAM, CMD_STA, CMD_OUT | (len(data) + 1), addr << 1]
|
|
packet += data
|
|
packet += [CMD_STO, CMD_END]
|
|
|
|
# One status byte per clocked-out byte: the address plus the payload.
|
|
reply = self._xfer(packet, len(data) + 1)
|
|
if reply[0] != ACK:
|
|
raise IOError(f"No ACK from I2C address 0x{addr:02x} on write")
|
|
if any(byte != ACK for byte in reply[1:]):
|
|
raise IOError(
|
|
f"I2C address 0x{addr:02x} NACKed a data byte "
|
|
f"(status {reply.hex(' ')})"
|
|
)
|
|
|
|
def read(self, addr, count):
|
|
"""Read count bytes from a 7-bit address. Raises IOError on NACK."""
|
|
if not 1 <= count <= MAX_XFER:
|
|
raise ValueError(f"Read of {count} bytes outside 1..{MAX_XFER}")
|
|
|
|
packet = [CMD_STREAM, CMD_STA, CMD_OUT | 1, (addr << 1) | 1]
|
|
# All but the last byte are ACKed by us; the final bare CMD_IN NACKs to
|
|
# tell the slave to stop.
|
|
if count > 1:
|
|
packet.append(CMD_IN | (count - 1))
|
|
packet += [CMD_IN, CMD_STO, CMD_END]
|
|
|
|
reply = self._xfer(packet, count + 1)
|
|
if reply[0] != ACK:
|
|
raise IOError(f"No ACK from I2C address 0x{addr:02x} on read")
|
|
return reply[1:]
|
|
|
|
def probe(self, addr):
|
|
"""Return True if a device ACKs its address. A NACK is not an error."""
|
|
packet = [CMD_STREAM, CMD_STA, CMD_OUT | 1, addr << 1, CMD_STO, CMD_END]
|
|
try:
|
|
return self._xfer(packet, 1)[0] == ACK
|
|
except IOError:
|
|
return False
|
|
|
|
def scan(self, first=0x08, last=0x77):
|
|
"""Return the addresses in [first, last] that respond."""
|
|
return [addr for addr in range(first, last + 1) if self.probe(addr)]
|