Source code for openbricks.drivers.tca9548a

# SPDX-License-Identifier: MIT
"""
TI TCA9548A 8-channel I2C multiplexer / switch.

Some I2C sensors have a fixed address with no way to change it — the
TCS34725 colour sensor is stuck at ``0x29``, for instance — so you
cannot put two of them on one bus without an address collision. The
TCA9548A sits between the host and the sensors and fans one bus out to
eight electrically-isolated channels (SD0/SC0 .. SD7/SC7); each channel
can host its own copy of the same-address device.

The mux itself answers at ``0x70`` by default (``0x70``..``0x77`` via
the A0/A1/A2 address pins). Channel selection is a single-byte write to
that address: bit *N* enables channel *N*. Multiple bits enable several
channels at once; ``0x00`` disables all of them.

The drop-in path is ``mux[n]``: it returns an object that quacks like a
``machine.I2C`` bus but transparently selects channel ``n`` before every
operation, so existing drivers construct against it unchanged::

    from machine import I2C, Pin
    from openbricks.drivers.tca9548a import TCA9548A
    from openbricks.drivers.tcs34725 import TCS34725

    i2c = I2C(0, sda=Pin(21), scl=Pin(22), freq=400_000)
    mux = TCA9548A(i2c)
    left  = TCS34725(mux[0])   # 0x29 on channel 0
    right = TCS34725(mux[1])   # 0x29 on channel 1

Reference: TCA9548A datasheet (Texas Instruments), section 8.3.
"""

_DEFAULT_ADDR = 0x70
_NUM_CHANNELS = 8


[docs] class TCA9548A: """8-channel I2C multiplexer for same-address sensors. Index it like a list: ``mux[n]`` returns an I2C-compatible handle for channel *n* (0-7) that selects the channel transparently before every transaction — pass it to any driver in place of the real ``machine.I2C``. Args: i2c: the upstream ``machine.I2C`` bus the mux sits on. address: mux's own I2C address, 0x70-0x77 via A0/A1/A2 straps (default 0x70). """ def __init__(self, i2c, address=_DEFAULT_ADDR): self._i2c = i2c self._addr = address # Cache the last control byte written so repeated access to the # same channel doesn't re-issue the select. ``None`` means # "unknown" — the first select always writes. self._selected = None
[docs] def select(self, channel): """Enable exactly ``channel`` (0..7), disabling the rest.""" mask = 1 << _check_channel(channel) if mask != self._selected: # Unknown until the write SUCCEEDS. If it raises, the # mux's real register state is anybody's guess (the write # may have half-happened, or the mux may have reset) — a # cache still claiming the old channel would make the next # select skip its write and silently talk to the wrong # channel. self._selected = None self._i2c.writeto(self._addr, bytes([mask])) self._selected = mask
[docs] def disable(self): """Disable every channel (control byte ``0x00``).""" if self._selected != 0: self._i2c.writeto(self._addr, b"\x00") self._selected = 0
def __getitem__(self, channel): """Return a ``machine.I2C``-like proxy bound to ``channel``.""" return _MuxChannel(self, _check_channel(channel))
def _check_channel(channel): if not 0 <= channel < _NUM_CHANNELS: raise ValueError( "channel must be 0..%d, got %r" % (_NUM_CHANNELS - 1, channel)) return channel class _MuxChannel: """A view of one mux channel that quacks like ``machine.I2C``. Every bus method selects the channel on the parent mux first, then delegates to the real I2C bus, so a driver that only ever touches this proxy addresses its own channel's device transparently. """ def __init__(self, mux, channel): self._mux = mux self._channel = channel def _select(self): try: self._mux.select(self._channel) except OSError as e: # The MUX itself didn't ACK — a different failure from the # device behind it not answering, and the message must say # so (a bare ENODEV made the two indistinguishable on the # bench; a whole session went to telling them apart). raise OSError( e.args[0] if e.args else 19, "mux at 0x%02x did not ACK selecting channel %d" % (self._mux._addr, self._channel)) def _no_ack(self, e, addr): """Re-raise a device-side OSError with the channel + address that failed. Same errno; the extra text is what turns ``[Errno 19] ENODEV`` into a named sensor.""" raise OSError( e.args[0] if e.args else 19, "no ACK from device 0x%02x on mux channel %d" % (addr, self._channel)) def readfrom_mem(self, addr, reg, nbytes, *args, **kwargs): self._select() try: return self._mux._i2c.readfrom_mem( addr, reg, nbytes, *args, **kwargs) except OSError as e: self._no_ack(e, addr) def writeto_mem(self, addr, reg, data, *args, **kwargs): self._select() try: return self._mux._i2c.writeto_mem( addr, reg, data, *args, **kwargs) except OSError as e: self._no_ack(e, addr) def readfrom(self, addr, nbytes, *args, **kwargs): self._select() try: return self._mux._i2c.readfrom(addr, nbytes, *args, **kwargs) except OSError as e: self._no_ack(e, addr) def writeto(self, addr, buf, *args, **kwargs): self._select() try: return self._mux._i2c.writeto(addr, buf, *args, **kwargs) except OSError as e: self._no_ack(e, addr) def scan(self): self._select() return self._mux._i2c.scan()