How to Use the TCA9548A I2C Multiplexer in MicroPython
If you tinker with microcontrollers and sensors, you’ll quickly run into a sneaky I2C problem: what do you do when you need to connect several identical I2C devices, all with the same hardwired address? “Address conflict” is the arch-nemesis of even the most optimistic electronics enthusiast.
Thankfully, the TCA9548A I2C multiplexer swoops in like a superhero for exactly this kind of situation!
Meet the TCA9548A: Your I2C Channel Switchboard
The TCA9548A is an integrated circuit specifically designed to expand your I2C capabilities. It acts like a switchboard operator, providing eight selectable I2C channels. This means you can have eight sets of devices that all use the same address — just not at the same time. With a quick software command, you can switch the active channel and communicate with the devices you care about right now.
It’s especially handy for sensor arrays, robotics, and projects that need lots of identical modules!
Its default I2C address is 0x70
(or 112
in decimal). I purchased this 6-pack on Amazon and in the examples below, used it with a Raspberry Pi Pico.
How It Works: Talking to the Multiplexer
The TCA9548A is blissfully simple. You just write a single byte to it, with a specific bit set for the channel you want to activate. Here’s the core idea in MicroPython, using the machine module:
import machine
# Set up I2C (adjust pins for your board)
i2c = machine.I2C(0, sda=machine.Pin(16), scl=machine.Pin(17))
# Activate channel 0 (bit 0)
i2c.writeto(0x70, bytes([1 << 0]))
print("Devices on channel 0:", str(i2c.scan()))
When you scan the I2C bus after selecting a channel, you’ll see the multiplexer itself (0x70
) and any devices connected on that channel.
A subtle but vital point: you must use the 1 << channel
notation. Writing a simple zero won’t switch anything. For every channel, set only its corresponding bit:
i2c.writeto(0x70, bytes([1 << 0])) # channel 0
i2c.writeto(0x70, bytes([1 << 1])) # channel 1
i2c.writeto(0x70, bytes([1 << 2])) # channel 2
# and so on up to channel 7
Basic Example: Scanning All Channels
Let’s cycle through all eight channels and scan for devices on each. Here’s how you do it:
import machine
# Initialize I2C
i2c = machine.I2C(0, sda=machine.Pin(16), scl=machine.Pin(17))
for channel in range(8):
i2c.writeto(0x70, bytes([1 << channel]))
devices = i2c.scan()
print(f"Devices on channel {channel}: {devices}")
This straightforward loop helps you quickly discover what’s where on your multiplexer.
Advanced Example: Using Two Identical Time-of-Flight Sensors
Imagine you have two VL53L0X time-of-flight distance sensors. Both use the same I2C address, so you can’t connect them to a single I2C bus — unless you use the TCA9548A.
Here’s how to do it in MicroPython (requires the vl53l0x.py driver):
import time
import machine
import vl53l0x
# Set up I2C as before
i2c = machine.I2C(0, sda=machine.Pin(16), scl=machine.Pin(17))
# Select channel 0 and create first sensor object
i2c.writeto(0x70, bytes([1 << 0]))
tof0 = vl53l0x.VL53L0X(i2c)
# Select channel 1 and create second sensor object
i2c.writeto(0x70, bytes([1 << 1]))
tof1 = vl53l0x.VL53L0X(i2c)
while True:
# Read from sensor 0
i2c.writeto(0x70, bytes([1 << 0]))
distance_sensor0 = tof0.ping() - 50
# Read from sensor 1
i2c.writeto(0x70, bytes([1 << 1]))
distance_sensor1 = tof1.ping() - 50
print(f"S1: {distance_sensor0} mm, S2: {distance_sensor1} mm")
time.sleep(0.1)
The trick here is to always switch to the correct channel before accessing a sensor, and to use separate class instances for each sensor. Otherwise, things get mixed up!
Wrap Up: Flexible, Simple, Essential
The TCA9548A may look humble, but it unlocks a world of possibilities for your I2C projects. No more address conflicts! More sensors, more flexibility, less stress.
If you build something fun with this chip, share your experience in the comments!
For more information, visit my sample code on GitHub.
Happy multiplexing!