Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TCA9548A I2C Multiplexer #36

Open
4 tasks
Tracked by #30
Mikefly123 opened this issue Oct 21, 2024 · 0 comments
Open
4 tasks
Tracked by #30

TCA9548A I2C Multiplexer #36

Mikefly123 opened this issue Oct 21, 2024 · 0 comments
Labels
new component Creating a new component for this task sensor Relating to a sensor implementation

Comments

@Mikefly123
Copy link
Member

Mikefly123 commented Oct 21, 2024

What Does This Component Do?

The TCA9548A takes an input I2C bus and splits it into up to eight more I2C buses! We use this for two reasons:

  1. Allowing us to repetitively use the same I2C devices without having to set unique addresses on all of them (see all the sensors on each solar panel!)
  2. Providing some level of bus protection, so the primary I2C bus is insulated from failures of downstream devices (i.e. if we lose a temp sensor on one solar panel face it shouldn't block getting data from all other solar panel faces)

Beware that this device (or at least the CIrcuitPython implementation of it!) is pretty finicky though and will unexpectedly hang the code if not initialized correctly. See the #25 Cube Killer on the CircuitPython repo for an example of this.

This component should:

  • Initialize the Sensor
  • Facilitate the creation of mux'd I2C buses and pass those through to other components
  • Allow for the deint / reint of the daughter I2C buses when needed
  • Scan the daughter I2C buses for active devices

Design Notes

TCA Datasheet
Adafruit TCA Guide

The way the TCA works kinda interesting in that the driver for this device is just a nested I2C driver. As in what you do is you send to an I2C address a port and another I2C address with whatever nested command you want to be passed through. If this is confusing I can try to make a diagram that describes this behavior.

This development effort might go hand in hand with the creation of a custom I2C driver for fprime-proves.

Example CircuitPython Implementation

From the tea9548a.py

import time
from micropython import const

try:
    from typing import List
    from typing_extensions import Literal
    from circuitpython_typing import WriteableBuffer, ReadableBuffer
    from busio import I2C
except ImportError:
    pass

_DEFAULT_ADDRESS = const(0x70)

__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_TCA9548A.git"


class TCA9548A_Channel:
    """Helper class to represent an output channel on the TCA9548A and take care
    of the necessary I2C commands for channel switching. This class needs to
    behave like an I2CDevice."""

    def __init__(self, tca: "TCA9548A", channel: int) -> None:
        self.tca = tca
        self.channel_switch = bytearray([1 << channel])

    def try_lock(self) -> bool:
        """Pass through for try_lock."""
        while not self.tca.i2c.try_lock():
            time.sleep(0)
        self.tca.i2c.writeto(self.tca.address, self.channel_switch)
        return True

    def unlock(self) -> bool:
        """Pass through for unlock."""
        self.tca.i2c.writeto(self.tca.address, b"\x00")
        return self.tca.i2c.unlock()

    def readfrom_into(self, address: int, buffer: ReadableBuffer, **kwargs):
        """Pass through for readfrom_into."""
        if address == self.tca.address:
            raise ValueError("Device address must be different than TCA9548A address.")
        return self.tca.i2c.readfrom_into(address, buffer, **kwargs)

    def writeto(self, address: int, buffer: WriteableBuffer, **kwargs):
        """Pass through for writeto."""
        if address == self.tca.address:
            raise ValueError("Device address must be different than TCA9548A address.")
        return self.tca.i2c.writeto(address, buffer, **kwargs)

    def writeto_then_readfrom(
        self,
        address: int,
        buffer_out: WriteableBuffer,
        buffer_in: ReadableBuffer,
        **kwargs
    ):
        """Pass through for writeto_then_readfrom."""
        # In linux, at least, this is a special kernel function call
        if address == self.tca.address:
            raise ValueError("Device address must be different than TCA9548A address.")
        return self.tca.i2c.writeto_then_readfrom(
            address, buffer_out, buffer_in, **kwargs
        )

    def scan(self) -> List[int]:
        """Perform an I2C Device Scan"""
        return self.tca.i2c.scan()


class TCA9548A:
    """Class which provides interface to TCA9548A I2C multiplexer."""

    def __init__(self, i2c: I2C, address: int = _DEFAULT_ADDRESS) -> None:
        self.i2c = i2c
        self.address = address
        self.channels = [None] * 8

    def __len__(self) -> Literal[8]:
        return 8

    def __getitem__(self, key: Literal[0, 1, 2, 3, 4, 5, 6, 7]) -> "TCA9548A_Channel":
        if not 0 <= key <= 7:
            raise IndexError("Channel must be an integer in the range: 0-7.")
        if self.channels[key] is None:
            self.channels[key] = TCA9548A_Channel(self, key)
        return self.channels[key]

In the pysquared.py

import adafruit_tca9548a  # I2C Multiplexer
...
class Satellite:
    def __init__(self):
        ...
        """
        TCA Multiplexer Initialization
        """
        try:
            self.tca = adafruit_tca9548a.TCA9548A(self.i2c1, address=int(0x77))
            self.hardware["TCA"] = True
        except OSError:
            self.error_print(
                "[ERROR][TCA] TCA try_lock failed. TCA may be malfunctioning."
            )
            self.hardware["TCA"] = False
            return
        except Exception as e:
            self.error_print("[ERROR][TCA]" + "".join(traceback.format_exception(e)))


    """
    Init Helper Functions
    """

    def scan_tca_channels(self):
        if not self.hardware["TCA"]:
            self.debug_print("[WARNING] TCA not initialized")
            return

        channel_to_face = {
            0: "Face0",
            1: "Face1",
            2: "Face2",
            3: "Face3",
            4: "Face4",
            5: "CAM",
        }

        for channel in range(len(channel_to_face)):
            try:
                self._scan_single_channel(channel, channel_to_face)
            except OSError:
                self.error_print(
                    "[ERROR][TCA] TCA try_lock failed. TCA may be malfunctioning."
                )
                self.hardware["TCA"] = False
                return
            except Exception as e:
                self.error_print(f"[ERROR][FACE]{traceback.format_exception(e)}")

    def _scan_single_channel(self, channel, channel_to_face):
        if not self.tca[channel].try_lock():
            return

        try:
            self.debug_print(f"Channel {channel}:")
            addresses = self.tca[channel].scan()
            valid_addresses = [
                addr for addr in addresses if addr not in [0x00, 0x19, 0x1E, 0x6B, 0x77]
            ]

            if not valid_addresses and 0x77 in addresses:
                self.error_print(f"No Devices Found on {channel_to_face[channel]}.")
                self.hardware[channel_to_face[channel]] = False
            else:
                self.debug_print([hex(addr) for addr in valid_addresses])
                if channel in channel_to_face:
                    self.hardware[channel_to_face[channel]] = True
        except Exception as e:
            self.error_print(f"[ERROR][FACE]{traceback.format_exception(e)}")
        finally:
            self.tca[channel].unlock()

Reference Schematic

Mux Board V1 Schematic

Screenshot 2024-11-04 at 10 58 35 PM

Required Hardware

  • Battery Board V3 and earlier OR a Mux Driver Board V1
  • Flight Controller Board (if using a Mux Driver Board)
@Mikefly123 Mikefly123 mentioned this issue Oct 21, 2024
12 tasks
@Mikefly123 Mikefly123 added sensor Relating to a sensor implementation new component Creating a new component for this task labels Oct 21, 2024
@Mikefly123 Mikefly123 added this to the fprime-proves-v1.0 milestone Oct 21, 2024
@Mikefly123 Mikefly123 added the draft Currently being drafted label Oct 21, 2024
@Mikefly123 Mikefly123 removed the draft Currently being drafted label Nov 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new component Creating a new component for this task sensor Relating to a sensor implementation
Projects
None yet
Development

No branches or pull requests

1 participant