# 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:
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:
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):
self._mux.select(self._channel)
def readfrom_mem(self, addr, reg, nbytes, *args, **kwargs):
self._select()
return self._mux._i2c.readfrom_mem(addr, reg, nbytes, *args, **kwargs)
def writeto_mem(self, addr, reg, data, *args, **kwargs):
self._select()
return self._mux._i2c.writeto_mem(addr, reg, data, *args, **kwargs)
def readfrom(self, addr, nbytes, *args, **kwargs):
self._select()
return self._mux._i2c.readfrom(addr, nbytes, *args, **kwargs)
def writeto(self, addr, buf, *args, **kwargs):
self._select()
return self._mux._i2c.writeto(addr, buf, *args, **kwargs)
def scan(self):
self._select()
return self._mux._i2c.scan()