TCA9548A – I2C Multiplexer

Über den Beitrag

I2C Bausteine haben meistens einen Satz fester Adressen, die sich über Adresspins oder -jumper einstellen lassen. Einige, wie z. B. der Portexpander MCP23017, sind mit 8 Adressen recht üppig ausgestattet. Andere hingegen, wie z. B. der Helligkeitssensor BH1750FVI haben nur 2 Adressen zur Auswahl. Aber was, wenn ihr damit nicht auskommt? Eine sehr komfortable Lösung dieses Problems ist der I2C Multiplexer TCA9548A, den ich in diesem Beitrag vorstellen werde. Darüber hinaus zeige ich euch, wie ihr alternativ mit einem einfachen MOSFET I2C Multiplexing realisieren könnt.

Der Beitrag ist folgendermaßen gegliedert:

  • Technische Eigenschaften des TCA9548A
  • Anschluss des TCA9548A an den Mikrocontroller
  • Ansteuerung – Grundlagen
  • Verwendung eines einzelnen TCA9548A:
    • ein I2C Bauteil pro Kanal
    • mehrere I2C Bauteile mit unterschiedlichen Adressen pro Kanal
  • Verwendung mehrerer TCA9548A
  • I2C Multiplexing mit MOSFETs

Technische Eigenschaften des TCA9548A

Der TCA9548A erlaubt es euch mithilfe seiner acht Kanäle ebenso viele I2C Bauteile mit identischer Adresse an einem I2C Bus zu betreiben. Für den TCA9548A selbst könnt ihr acht Adressen nach dem folgenden Schema einstellen:

  • 1 1 1 0 A2 A1 A0
    • Ax = 0 wenn an LOW, Ax = 1 wenn an HIGH
    • Mögliche Adressen sind damit: 1110000 bis 1110111 oder 0x70 bis 0x77

Mit acht TCA9548A könntet ihr also 64 I2C Bauteile betreiben, die alle dieselbe I2C Adresse haben.

TCA9548A Modul
TCA9548A Modul

Auf der Mikrocontrollerseite (Anschlüsse SDA / SCL) sind Pull-Up Widerstände integriert, die die Spannung auf das VIN Niveau ziehen. Die acht Kanäle besitzen keine integrierten Pull-Ups. Was zunächst wie ein Nachteil aussieht, ist ein Vorteil, denn es erlaubt euch mit verschiedenen Busspannungen zu arbeiten. Wählt beispielsweise 3.3 Volt für VIN und damit auch für die I2C Leitung zum Mikrocontroller. Davon unabhängig könnt ihr dann die einzelnen Kanäle mit externen Pull-Up Widerständen auf z. B. 5 Volt hochziehen.

Hier weitere technische Eigenschaften im Überblick:

  • Spannungsversorgung: 1.65 – 5.5 Volt
  • Maximale I2C Busfrequenz: 400 kHz
  • Low-aktiver Reset Pin
  • Alle Eingänge 5V tolerant
  • Mehrere oder auch alle Kanäle können gleichzeitig aktiviert werden

Für weitere Details schaut ins technische Datenblatt.

Ihr bekommt den TCA9548A als Modul für wenige Euro in Online-Shops wie Amazon oder AliExpress.

Anschluss des TCA9548A an den Mikrocontroller

Im folgenden Schema seht ihr am Beispiel eines Arduino UNO, wie ihr den TCA9548A anschließt. Der besseren Übersicht halber habe ich nur ein einziges I2C Bauteil abgebildet.

Das HIGH Niveau der Arduino UNO I2C-Leitungen liegt bei 5 Volt. Entsprechend sollte auch VIN mit 5 Volt versorgt werden. Die Adresspins sind in diesem Beispiel unverbunden und damit auf GND Niveau. Die Adresse ist damit 0x70. Auf der Seite des I2C „Sklaven“ Seite werden Pull-Ups benötigt, falls das angeschlossene Bauteil sie nicht mitbringt.  

Ansteuerung – Grundlagen

Der TCA9548A besitzt nur ein einziges, durch euch beschreibbares Register, nämlich das Kontrollregister:

Kontrollregister des TCA9548A
Kontrollregister des TCA9548A

Eine „1“ bedeutet, dass der Kanal aktiv ist, eine „0“ bedeutet, dass der Kanal inaktiv ist. Schreibt ihr beispielsweise eine 151 in das Register, ist das binär 10010111, sprich die Kanäle 0,1,2,4 und 7 sind aktiv. Um bestimmte Bits des Registers zu setzen, verwendet ihr am besten binäre Operationen:

Control\; Register=151 = (1<<7) |(1<<4)|(1<<2)|(1<<1)|(1<<0)

In den Beispielen dieses Beitrages ist aber immer nur jeweils ein Kanal aktiv. Der folgende Minisketch öffnet Kanal 0, indem er das Bit 0 des Kontrollregisters setzt:

#include<Wire.h>
#define TCA9548A_I2C_ADDRESS  0x70
#define TCA9548A_CHANNEL_0    0

void setup() {
  Wire.begin();
  setTCAChannel(TCA9548A_CHANNEL_0);
}

void loop() { 
}

void setTCAChannel(byte i){
  Wire.beginTransmission(TCA9548A_I2C_ADDRESS);
  Wire.write(1 << i);
  Wire.endTransmission();  
}

 

Zum Einstieg könnt ihr Folgendes probieren: Nehmt euer Lieblings-I2C Bauteil und baut die obige Schaltung nach. Dann ladet einen I2C Scanner Sketch hoch. So etwas findet ihr z.B. hier. Der Scanner sollte die Adresse 0x70 finden. Dann ladet ihr den TCA9548A_set_channel Sketch hoch, ohne den TCA9548A zwischendurch vom Strom zu trennen. Dann ladet ihr wieder den Scanner Sketch hoch. Er sollte dann die 0x70 und die Adresse eures I2C Bauteils anzeigen.

Im Grunde ist damit schon alles erklärt. Ihr müsst lediglich die Kanäle gezielt an- und ausschalten, um das jeweils gewünschte I2C Bauteil anzusprechen. Und wenn ihr eine „0“ in das Kontrollregister schreibt, sind alle Kanäle deaktiviert. In der Praxis können die Dinge aber schnell unübersichtlich werden, insbesondere wenn jedes I2C Bauteil als Objekt angelegt wird.

Einen einzelnen TCA9548A verwenden

Ein TCA9548A mit einem I2C Bauteil pro Kanal

In den folgenden Beispielen verwende ich den A/D Wandler ADS1115 als I2C Baustein. Ihr müsst euch nicht mit den Details des ADS1115 auseinandersetzen, um den Beitrag nachvollziehen zu können. Nehmt einfach zur Kenntnis, dass jeder einzelne ADS1115 initialisiert werden muss und ein paar Einstellungen benötigt (setVoltageRange_mV, setCompareChannels und setMeasureMode).

Die Beschaltung ist hinsichtlich der I2C Leitungen wohl keine Überraschung. Die orangefarbenen Leitungen und die Widerstände dienen nur dazu, ein paar unterschiedliche Spannungen am Eingang A0 messen zu können. Da die Adresspins der ADS1115 Module unverbunden sind, ist die I2C Adresse für alle Module 0x48.

TCA9548A - 4 I2C Kanäle, 4 I2C Geräte
TCA9548A – 4 I2C Kanäle, 4 I2C Geräte
TCA9548: und so sah der Aufbau real aus.
Und so sah es in der Realität aus

Im zugehörigen Sketch wird für jeden ADS1115 ein individuelles Objekt erzeugt. Es folgt die Initialisierung und die Einstellung der Parameter. In der Hauptschleife werden die Messwerte der ADS1115 Module abgefragt und ausgegeben.

#include<ADS1115_WE.h> 
#include<Wire.h>
#define AD1115_I2C_ADDRESS 0x48
#define TCA_I2C_ADDRESS    0x70

ADS1115_WE adc_0(AD1115_I2C_ADDRESS);
ADS1115_WE adc_1(AD1115_I2C_ADDRESS);
ADS1115_WE adc_2(AD1115_I2C_ADDRESS);
ADS1115_WE adc_3(AD1115_I2C_ADDRESS);

void setup() {
  Wire.begin();
  Serial.begin(9600);
  
  setTCAChannel(0);
  if(!adc_0.init()){
    Serial.print("ADS1115 No 0 not connected!");
  }
  adc_0.setVoltageRange_mV(ADS1115_RANGE_6144); // setting voltage range
  adc_0.setCompareChannels(ADS1115_COMP_0_GND); // setting adc channel
  adc_0.setMeasureMode(ADS1115_CONTINUOUS); // setting adc mode

  setTCAChannel(1);
  if(!adc_1.init()){
      Serial.print("ADS1115 No 1 not connected!");
  }
  adc_1.setVoltageRange_mV(ADS1115_RANGE_6144); // setting parameters
  adc_1.setCompareChannels(ADS1115_COMP_0_GND);
  adc_1.setMeasureMode(ADS1115_CONTINUOUS); 

  setTCAChannel(2);
  if(!adc_2.init()){
      Serial.print("ADS1115 No 2 not connected!");
  }
  adc_2.setVoltageRange_mV(ADS1115_RANGE_6144); // setting parameters
  adc_2.setCompareChannels(ADS1115_COMP_0_GND);
  adc_2.setMeasureMode(ADS1115_CONTINUOUS); 

  setTCAChannel(3);
  if(!adc_3.init()){
    Serial.print("ADS1115 No 3 not connected!");
  }
  adc_3.setVoltageRange_mV(ADS1115_RANGE_6144); // setting parameters
  adc_3.setCompareChannels(ADS1115_COMP_0_GND);
  adc_3.setMeasureMode(ADS1115_CONTINUOUS); 
}

void loop() {
  float voltage = 0.0;
  
  setTCAChannel(0);
  voltage = adc_0.getResult_V();
  Serial.print("Voltage [V], ADS1115 No 0: ");
  Serial.println(voltage);
  
  setTCAChannel(1);
  voltage = adc_1.getResult_V();
  Serial.print("Voltage [V], ADS1115 No 1: ");
  Serial.println(voltage);
  
  setTCAChannel(2);
  voltage = adc_2.getResult_V();
  Serial.print("Voltage [V], ADS1115 No 2: ");
  Serial.println(voltage);
  
  setTCAChannel(3);
  voltage = adc_3.getResult_V();
  Serial.print("Voltage [V], ADS1115 No 3: ");
  Serial.println(voltage);
  
  Serial.println("****************************");  
  delay(1000);
}

void setTCAChannel(byte i){
  Wire.beginTransmission(TCA_I2C_ADDRESS);
  Wire.write(1 << i);
  Wire.endTransmission();  
}

 

Ausgabe der Sketche

Die Ausgabe dieses und aller folgenden Sketche ist gleich:

TCA9548A Beispielsketch:
Ausgabe TCA9548A_4_channels
Ausgabe TCA9548A_4_channels

Einige Vereinfachungen

Mit dem letzten Sketch wollte ich das Prinzip verdeutlichen. Der Code lässt sich noch deutlich kürzen. In der folgenden, vereinfachten Version habe ich die ADS1115 Objekte als Array definiert und mehrfach verwendeten Code in Funktionen ausgelagert. 

#include<ADS1115_WE.h> 
#include<Wire.h>
#define AD1115_I2C_ADDRESS 0x48
#define TCA_I2C_ADDRESS    0x70

ADS1115_WE adc[4];

void setup() {
  Wire.begin();
  Serial.begin(9600);
  
  for(int i=0; i<4; i++){
    adc[i] =  ADS1115_WE(AD1115_I2C_ADDRESS);
    setTCAChannel(i);
    setupAdc(i);
  }
}

void loop() {
  float voltage = 0.0;
  
  for(int i=0; i<4; i++){
    setTCAChannel(i);
    voltage = adc[i].getResult_V();
    Serial.print("Voltage [V], ADS1115 No ");
    Serial.print(i);
    Serial.print(": ");
    Serial.println(voltage);
  }
  Serial.println("****************************");  
  delay(1000);
}

void setTCAChannel(byte i){
  Wire.beginTransmission(TCA_I2C_ADDRESS);
  Wire.write(1 << i);
  Wire.endTransmission();  
}

void setupAdc(byte i){
  if(!adc[i].init()){
    Serial.print("ADS1115 No ");
    Serial.print(i);
    Serial.println(" not connected!");
  }
  adc[i].setVoltageRange_mV(ADS1115_RANGE_6144);
  adc[i].setCompareChannels(ADS1115_COMP_0_GND);
  adc[i].setMeasureMode(ADS1115_CONTINUOUS); 
}

 

Weitere Vereinfachung: nur ein Objekt

Vielleicht ist dem einen oder anderen aufgefallen, dass alle Moduleinstellungen gleich sind. Es ist deswegen nicht notwendig, für jedes ADS1115 Modul ein eigenes Objekt zu erzeugen. Dadurch wird der Code noch einmal einfacher. Wenn die I2C Bauteile individuelle Einstellungen (Kalibrierfaktoren o. ä.) benötigen, ist diese Vereinfachung so nicht möglich.

#include<ADS1115_WE.h> 
#include<Wire.h>
#define AD1115_I2C_ADDRESS 0x48
#define TCA_I2C_ADDRESS    0x70

ADS1115_WE adc(AD1115_I2C_ADDRESS);

void setup() {
  Wire.begin();
  Serial.begin(9600);
  
  for(int i=0; i<4; i++){
    setTCAChannel(i);
    setupAdc(i);
  }
}

void loop() {
  float voltage = 0.0;
  
  for(int i=0; i<4; i++){
    setTCAChannel(i);
    voltage = adc.getResult_V();
    Serial.print("Voltage [V], ADS1115 No ");
    Serial.print(i);
    Serial.print(": ");
    Serial.println(voltage);
  }
  Serial.println("****************************");  
  delay(1000);
}

void setTCAChannel(byte i){
  Wire.beginTransmission(TCA_I2C_ADDRESS);
  Wire.write(1 << i);
  Wire.endTransmission();  
}

void setupAdc(byte i){
  if(!adc.init()){
    Serial.print("ADS1115 No ");
    Serial.print(i);
    Serial.println(" not connected!");
  }
  adc.setVoltageRange_mV(ADS1115_RANGE_6144);
  adc.setCompareChannels(ADS1115_COMP_0_GND);
  adc.setMeasureMode(ADS1115_CONTINUOUS); 
}

 

Ein TCA9548A mit mehreren I2C Geräten pro Kanal

Falls ihr mehr als acht I2C Bauteile ansteuern wollt und diese eine Einstellung der I2C Adresse zulassen, könnt ihr mehrere I2C Geräte pro Kanal benutzen. Der ADS1115 lässt die Einstellung vier verschiedener Adressen zu. Damit könntet ihr problemlos 32 ADS1115 Module mit einem einzigen TCA9548A betreiben. Ich zeige das Prinzip anhand von vier ADS1115 Modulen, die auf zwei Kanäle verteilt sind. Hier zunächst die Schaltung:

TCA9548A - 2 I2C Kanäle, 4 I2C Geräte
TCA9548A – 2 I2C Kanäle, 4 I2C Geräte

Beachtet, dass die Adresspins der ADCs 1 und 3 mit VCC verbunden sind. Deren Adresse ändert sich damit auf 0x49. Da die Einstellungen für die ADS1115 bis auf die I2C Adresse identisch sind, gäbe es auch hier Potenzial für weitere Vereinfachungen.

#include<ADS1115_WE.h> 
#include<Wire.h>
#define AD1115_I2C_ADDRESS_A  0x48
#define AD1115_I2C_ADDRESS_B  0x49
#define TCA_I2C_ADDRESS       0x70

ADS1115_WE adc[4];

void setup() {
  Wire.begin();
  Serial.begin(9600);
  
  setTCAChannel(0);
  adc[0] =  ADS1115_WE(AD1115_I2C_ADDRESS_A);
  setupAdc(0);
  adc[1] =  ADS1115_WE(AD1115_I2C_ADDRESS_B);
  setupAdc(1);
  
  setTCAChannel(1);
  adc[2] =  ADS1115_WE(AD1115_I2C_ADDRESS_A);
  setupAdc(2);
  adc[3] =  ADS1115_WE(AD1115_I2C_ADDRESS_B);
  setupAdc(3);
}

void loop() {
  float voltage = 0.0;
  
  for(int i=0; i<4; i++){
    if(i<2){
      setTCAChannel(0);
    }
    else{
      setTCAChannel(1);
    }
    voltage = adc[i].getResult_V();
    Serial.print("Voltage [V], ADS1115 No ");
    Serial.print(i);
    Serial.print(": ");
    Serial.println(voltage);
  }
  Serial.println("****************************");  
  delay(1000);
}

void setTCAChannel(byte i){
  Wire.beginTransmission(TCA_I2C_ADDRESS);
  Wire.write(1 << i);
  Wire.endTransmission();  
}

void setupAdc(byte i){
  if(!adc[i].init()){
    Serial.print("ADS1115 No ");
    Serial.print(i);
    Serial.println(" not connected!");
  }
  adc[i].setVoltageRange_mV(ADS1115_RANGE_6144);
  adc[i].setCompareChannels(ADS1115_COMP_0_GND);
  adc[i].setMeasureMode(ADS1115_CONTINUOUS); 
}

 

Mehrere TCA9548A verwenden

Es ist auch nicht schwierig, mehrere TCA9548A zu steuern. In meinem Beispiel dazu verwende ich zwei TCA9548A, an denen jeweils zwei ADS1115 Module in getrennten Kanälen hängen.

2 TCA9548A - je 2 I2C Kanäle
2 TCA9548A – je 2 I2C Kanäle

Bei Verwendung mehrerer TCA9548A müsst ihr darauf achten, dass immer nur einer von ihnen einen offenen Kanal hat. Ich habe den Sketch dazu recht allgemein geschrieben, sodass er mit geringen Anpassungen auf eine größere Anzahl an TCA9548A Module und I2C Bauteile übertragbar ist.

#include<ADS1115_WE.h> 
#include<Wire.h>
#define AD1115_I2C_ADDRESS   0x48
byte tcaI2CAddress[] = {0x70,0x71};
byte numberOfDevicesPerTCA = 2;
const int numberOfDevices = 4;

ADS1115_WE adc[numberOfDevices];

void setup() {
  Wire.begin();
  Serial.begin(9600);
  
  for(int i=0; i<numberOfDevices; i++){
    adc[i] =  ADS1115_WE(AD1115_I2C_ADDRESS);
    setupAdc(i);
  }
}

void loop() {
  float voltage = 0.0;
  
  for(int i=0; i<numberOfDevices; i++){
    byte tca = setTCAAndChannel(i);
    voltage = adc[i].getResult_V();
    Serial.print("Voltage [V], ADS1115 No ");
    Serial.print(i);
    Serial.print(": ");
    Serial.println(voltage);
    disableTCA(tca);
  }
  Serial.println("****************************");  
  delay(1000);
}

byte setTCAAndChannel(byte i){
  byte tca = i/numberOfDevicesPerTCA;
  byte channel = i%numberOfDevicesPerTCA;
  
  Wire.beginTransmission(tcaI2CAddress[tca]);
  Wire.write(1 << channel);
  Wire.endTransmission();

  return tca;
}

void disableTCA(byte tca){
  Wire.beginTransmission(tcaI2CAddress[tca]);
  Wire.write(0);
  Wire.endTransmission();  
}

void setupAdc(byte i){
  byte tca = setTCAAndChannel(i);
  
  if(!adc[i].init()){
    Serial.print("ADS1115 No ");
    Serial.print(i);
    Serial.println(" not connected!");
  }
  adc[i].setVoltageRange_mV(ADS1115_RANGE_6144);
  adc[i].setCompareChannels(ADS1115_COMP_0_GND);
  adc[i].setMeasureMode(ADS1115_CONTINUOUS);
  disableTCA(tca); 
}

 

Jetzt könnte man noch einen Schritt weiter gehen und mehrere TCA9548A, mehrere Kanäle und mehrere I2C Geräte pro Kanal verwenden. Ich spare mir das jedoch. Wenn ihr das Grundprinzip verstanden habt, sollte das kein Problem sein.

I2C Multiplexing mit MOSFETs

2N7000 MOSFET

Bei der Beschäftigung mit dem TCA9548A kam mir der Gedanke, ob man eine I2C Leitung nicht auch auf anderem Wege an- und ausschalten kann. Genauer gesagt geht es darum, die SDA Leitung, also die Datenübertragung gezielt zu unterbrechen. Zuerst kamen mir dabei Transistoren als Schalter in den Sinn, das funktionierte aber nicht. Was hingegen zum Erfolg führte, war der Einsatz der nahen Verwandten, den MOSFETs (Metal-Oxide Semiconductor Field-Effect Transistor).

Grundlagen zur Funktion von MOSFETs findet ihr hier. An dieser Stelle nur soviel: Der MOSFET hat drei Anschlüsse, nämlich Drain, Source und Gate. Den Stromfluss von Drain nach Source steuert ihr über die Spannung an Gate. Es gibt n- und p-Kanal MOSFETs. Bei einem n-Kanal MOSFET, wie dem von mir verwendeten IRF540, müsst ihr eine gewisse positive Mindestspannung an Gate anlegen, damit er öffnet. So funktioniert das Teil dann wie ein Schalter. Also vom Prinzip her ähnlich wie bei den Transistoren.

Wenn der MOSFET sperrt, dann muss die SDA Leitung trotzdem auf Spannung gehalten werden. Jedes Absenken würde als Versuch einer Datenübertragung gewertet. Deswegen sind Pull-Up Widerstände erforderlich. Hier eine schematische Darstellung:

MOSFETs als I2C Multiplexer: I/O 1 und 2 schalten die MOSFETs
MOSFETs als I2C Multiplexer: I/O 1 und 2 schalten die MOSFETs

Ich habe mir als Ziel gesetzt, vier ADS1115 mit zwei MOSFETs zu steuern. Dazu habe ich die folgende Schaltung verwendet:

Die TCA9548A Alternative: Mosfets
Die TCA9548A Alternative: Mosfets

Ich habe zunächst mit dem I2C Scanner und einem einzelnen MOSFET probiert, ob ich auf die hinter dem MOSFET liegenden ADS1115 Module zugreifen kann. Als MOSFET hatte ich einen IRF540 ausgewählt. Dann habe ich mich gewundert, dass das Ganze bei 100 kHz erst funktionierte als ich einen zusätzlichen 10 kOhm Pull-Up installierte. Bei 400 kHz musste ich zu 2.2 kOhm Pull-Ups greifen. Grund dafür ist die Kapazität des MOSFETs – darauf hat mich ein freundlicher Leser aufmerksam gemacht. Eine kleine Überschlagsrechnung: Der IRF540 hat bei 5 Volt eine Ausgangskapazität von ca. 1200 pF = 1.2×10-9 F. Die Ladezeit τ eines Kondensators hängt von seiner Kapazität C und dem Widerstand R (hier 10 kOhm) ab:

\tau = C\cdot R=1.2\cdot10^{-9}\cdot 10^4=1.2\cdot10^{-5}\;[\text{s}]

Der Kondensator ist nach 1 τ auf 63 % geladen. Bei 100 kHz Frequenz muss das HIGH Niveau nach einer Absenkung nach 10-5 s wieder erreicht sein. Passt also nicht! Mit 2 parallelen Pull-Ups von 10 kOhm ging es gerade eben. Am Oszilloskop war das schön zu sehen. Hier zunächst ohne MOSFETs:

I2C-Signale ohne MOSFET
I2C-Signale ohne MOSFET

Und so mit IRF540 und zwei parallelen 10 kOhm Pull-Ups:

I2C-Signale mit MOSFET
I2C-Signale mit IRF540 MOSFET und zwei 10 kOhm Pull-Ups

Nehmt also kleine MOSFETs wie zum Beispiel einen 2N7000 oder einen BS170. Die haben einen Bruchteil der Ausgangskapazität eines IRF540.

Mit Mikrocontrollern, die auf 3.3 Volt laufen, werdet ihr wahrscheinlich Schwierigkeiten bekommen, da es mit der notwendigen Gatespannung eng wird.

Nun zum Sketch: Da in diesem Beispiel zwei ADS1115 auf einer Leitung liegen, müssen wieder zwei unterschiedliche I2C Adressen verwendet werden. Ansonsten gibt es eigentlich nichts Neues, außer dass die Kanäle über die Steuerpins 8 und 9 geschaltet werden.

#include<ADS1115_WE.h> 
#include<Wire.h>
#define AD1115_I2C_ADDRESS_A  0x48
#define AD1115_I2C_ADDRESS_B  0x49
#define I2C_CHANNEL_0_PIN     8
#define I2C_CHANNEL_1_PIN     9

ADS1115_WE adc[4];

void setup() {
  Wire.begin();
  Serial.begin(9600);
  pinMode(I2C_CHANNEL_0_PIN, OUTPUT);
  pinMode(I2C_CHANNEL_1_PIN, OUTPUT);
  
  setI2CChannel(0);
  adc[0] =  ADS1115_WE(AD1115_I2C_ADDRESS_A);
  setupAdc(0);
  adc[1] =  ADS1115_WE(AD1115_I2C_ADDRESS_B);
  setupAdc(1);
  
  setI2CChannel(1);
  adc[2] =  ADS1115_WE(AD1115_I2C_ADDRESS_A);
  setupAdc(2);
  adc[3] =  ADS1115_WE(AD1115_I2C_ADDRESS_B);
  setupAdc(3);
}

void loop() {
  float voltage = 0.0;
  
  for(int i=0; i<4; i++){
    if(i<2){
      setI2CChannel(0);
    }
    else{
      setI2CChannel(1);
    }
    voltage = adc[i].getResult_V();
    Serial.print("Voltage [V], ADS1115 No ");
    Serial.print(i);
    Serial.print(": ");
    Serial.println(voltage);
  }
  Serial.println("****************************");  
  delay(1000);
}

void setI2CChannel(byte i){
  if(i==0){
    digitalWrite(I2C_CHANNEL_1_PIN, LOW);
    digitalWrite(I2C_CHANNEL_0_PIN, HIGH);  
  }
  else if(i==1){
    digitalWrite(I2C_CHANNEL_0_PIN, LOW);
    digitalWrite(I2C_CHANNEL_1_PIN, HIGH);  
  }
}

void setupAdc(byte i){
  if(!adc[i].init()){
      Serial.print("ADS1115 No ");
      Serial.print(i);
      Serial.println(" not connected!");
    }
    adc[i].setVoltageRange_mV(ADS1115_RANGE_6144);
    adc[i].setCompareChannels(ADS1115_COMP_0_GND);
    adc[i].setMeasureMode(ADS1115_CONTINUOUS); 
}

 

Auch hier gäbe es wieder Potenzial zur Vereinfachung, denn zumindest zwei ADS1115 sind in diesem Beispiel identisch.

Danksagung

Die Fritzing Bauteile ADS1115 und TCA9546A stammen von der Firma Adafruit. Wenn ihr auch mal Fritzing Bauteile sucht, dann habt ihr große Chancen hier fündig zu werden.

19 thoughts on “TCA9548A – I2C Multiplexer

  1. Hallo Wolfgang,

    eine weitere Möglichkeit, mehrere I2C-Bausteine, die die gleiche Adresse haben, anzuschließen, ist ein „Software-Multiplexer“. Wenn man die Bauteilanzahl minimieren möchte und nicht auf High-Speed-Kommunikation angewiesen ist, funktioniert das relativ gut. Man muss nicht mal was in den Sensor-Bibliotheken ändern. Wie es geht, habe ich in meinem Blog beschrieben: https://arduino-craft-corner.de/index.php/2023/12/14/software-i2c-multiplexer/

    Grüße
    Bernhard

    1. Hallo Bernhard, coole Lösung. Es ist immer hilfreich Lösungen zu haben, bei denen man Bauteile einsparen kann. „FlexWire“ kannte ich bislang nicht. Wieder was gelernt!
      VG, Wolfgang

  2. Hallo Wolfgang,
    klasse Artikel! Kleiner Fehler: im Bild „MOSFETs als I2C Multiplexer: I/O 1 und 2 schalten die MOSFETs“ müssen die Anschlüsse Drain und Source vertauscht werden, damit es wie gewollt funktioniert. Im nachfolgenden Bild „Die TCA9548A Alternative: Mosfets“ passt es – wobei es über die angedeutete Beschriftung und die Rundung des TO-92 (vermutlich) weniger einfach ersichtlich ist.
    Gruß Markus

    1. Hi Markus, stimmt! Gut gesehen. Source ist auf der Master-Seite, da die für ein Signal auf LOW geht. Werde ich ändern. Danke. VG, Wolfgang

  3. Hallo und vielen Dank für den tollen Beitrag.
    Bei meinem eigenen Projekt mit mehreren TCA bin ich auf ein Problem gestoßen, dass in deinem Sketch auch auftreten könnte.
    Der Kanal des TCA sollte jedes mal nachdem ein Kanal aktiviert wurde und die Nachricht an das I2C device weitergeleitet wurde, wieder zurückgesetzt werden.
    Wire.beginTransmission(tcaI2CAddress[tca]);
    Wire.write(0);
    Wire.endTransmission();
    Ansonsten wird jede weiter Nachricht auf dem i2c bus an das device am aktiven Kanal weitergeleitet. Vor allem bei mehreren multiplexern mit mehreren devices, die die gleiche Adresse haben, führt das zu Problemen, wie man sich denken kann.

  4. Hallo und vielen Dank für diese tolle Ausführung. Als Elektronik-Neuling freue ich mich beim Durchstöbern des Netzes über Beiträge, die strukturiert aufbauen und erklären. Ich bin deutlich gewachsen beim Lesen. Danke.

    Wie kann ich i2c-Sensoren hinter dem TCA9548a ansprechen, die vorm Auslesen erst etwas angeschrieben werden müssen? Wenn ich mit den i2c-tools in der Kommandozeile arbeite, dann kann ich einen Temperatursensor so auslesen:
    i2cset -y 1 0x77 0xf4 0x2e w
    i2cget -y 1 0x77 0xf6 w
    Der Sensor hat die Adresse 0x77 und ist am i2c Bus Nr. 1 angeschlossen. Das Controlregister 0xf4 wird erst mit dem Wert 0x2e belegt (das bedeutet dem Sensoren, die Temperatur in das Register 0xf6 zu legen. Mit den i2cget Befehl wird also dieses Register ausgelesen und voilá alles ist super.

    Aber wie soll das mit dem Multiplexer dazwischen laufen? An dem TCA habe ich auf Channel 7 (01000000) (0x80) den Sensor angeschlossen. Wenn ich mit i2c-tools folgendes probiere:
    i2cset -y 1 0x70 0x80
    i2cset -y 1 0x70 0xf4 0x2e w
    i2cget -y 1 0x70 0xf6 w
    erhalte ich nur 0 als Ausgabe.

    Zur Erklärung, was ich genau versuche, falls das von Interesse ist:
    Ich versuche, mein erstes Projekt ans Laufen zu bringen und habe dafür 5 Temperatur-und Druck-Sensoren mit i2c-Bus gekauft (BMP180). Da diese Sensoren feste Adressen haben (0x77) habe ich also auch nach etwas Recherche den Multiplexer TCA9548a gekauft. Ich habe einen Raspberry Pi 4 und bis jetzt nur in Python programmiert oder mit den i2c-tools in der Linux Befehlszeile gearbeitet. Über beide Varianten kann ich einen BMP180, direkt an den Pi angeschlossen, auslesen. Schließe ich den TCA dazwischen, komme ich aber an keine Daten dran.
    Der veraltete Pythontreiber für den BMP180 (heißt BMP085) tut es mit den alten i2c Modulen von adafruit mit einem direkt angeschlossenen Sensor. Der Pythontreiber für den TCA baut auf den neuen i2c Modulen von adafruit auf und kann nicht mit dem alten BMP085 Treiber verbunden werden. Den alten BMP085 Treiber habe ich also umgeschrieben, so dass dieser die neuen adafruit i2c Module nutzt. Mit dem selbstgeschriebenen Treiber krige ich auch perfekte Temperaturwerte, wenn ein Sensor direkt angeschlossen ist. Baue ich das ganze mit dem TCA dazwischen auf, klappt es nicht. Also habe ich versucht, über die i2c-tools in der Befehlszeile die Grundlagen herauszufinden. Mit einem direkt angeschlossenen Sensor klappt das. Aber ich weiß nicht, wie ich wann welche Adresse des TCA als „Mittelsmann“ ansprechen muss, damit ich Werte kriege.

    1. Hallo Jo, erstmal muss ich sagen, dass ich nicht viel Erfahrung mit Kommandozeilen-Operationen habe. Deswegen kann ich dir keine ganz sichere Antwort geben. Das erste Kommando erscheint mir richtig
      2cset -y 1 0x70 0x80
      um den Kanal 7 einzustellen. Damit ist der Weg frei zum angeschlossenen Sensor. Bei den folgenden Kommandos musst du dann die Adresse des Sensors (0x77) und nicht die des TCA (0x70) verwenden. Mit der 0x70 wird erneut der TCA angesprochen und nicht der Sensor.

      1. Perfekt. Danke. Das tut.
        Dann ist der Multiplexer noch einfacher zu vestehen, als ich dachte. Dieser Multiplexer ist nichts anderes als ein Schalter, der zwischen 8 Kanälen schaltet. Der Schaltvorgang bzw. die Kanalwahl wird simpler Weise über i2c übertragen. Jetzt verstehe ich erst Deine Gedanken zum Selbstbauen mit MOSFETs… Allerdings verstehe ich dann nicht, warum der Multiplexer für jeden Kanal sowohl einen sda wie auch einen scl Ausgang hat. sda allein würde ja reichen, da scl immer auf allen Kanälen gleich sein sollte.
        Ich dachte erst, dass der Multiplexer eher wie eine Art Netzwerk-Gateway fungiert und Daten weiterleitet, indem der Multiplexer über seine i2c Adresse angesprochen wird und dann an eine neue Adresse weiterleitet. (Das wäre echt sehr kompliziert…) In Deinem C Beispiel-Code kriegen die Sensorstrukturen halt beim Initiieren die Sensoradresse. Beim Aufrufen von Methoden für diese Strukturen muss die Adresse nicht mehr als Parameter übergeben werden… da habe ich nicht sofort geschnallt, dass nicht mehr die Multiplexer Adresse „drin“ ist. … genau lesen hilft…

        Übrigens: In Deinem Diagramm: „2 TCA9548A – je 2 I2C Kanäle“ hast Du einen Multiplexer falsch angeschlossen – da geht etwas ungewollt auf RESET.

        Mille Grazie noch mal und beste Grüße.

        1. Schön, dass es funktioniert! Du hast recht, der TCA9548 ist eigentlich eine Art Schalter. Und ich gebe dir auch darin Recht, dass im Prinzip eine gemeinsam genutzte SCL Leitung ausreichend wäre.
          Und danke für den Hinweis wegen des Diagramms. Da ist tatsächlich was verrutscht.

  5. danke für den Beitrag!
    Sind die I2C Kanäle untereinander galvanisch getrennt?
    in anderen Worten: wenn ich eine zu hohe Spannung auf einem Kanal habe, bekommen es alle anderen Teilnehmer inkl. Arduino mit und gehen dadurch kaputt?

    1. Hallo, was bei zu hoher Spannung passiert bzw. wie gut die Kanäle voneinander getrennt sind kann ich nicht sagen. Ich würde vermuten, dass einfach der TCA9548 zerstört wird. Das Datenblatt lässt sich nicht im Detail darüber aus, wie die Umschalter im Chip genau aussehen. Das könnte Rückschlüsse zulassen. Vielleicht guckst du selber noch einmal im Datenblatt, ob du Hinweise findest.

      Oder ging die Frage in die Richtung, was passiert, wenn unterschiedliche Kanäle unterschiedliche Spannungen haben? Sprich, an dem einen Kanal hängt ein 5V I2C Bauteil und einem anderen ein 3.3V Bauteil? Das sollte kein Problem sein, das Datenblatt sagt: „The TCA9548A may also be used for voltage translation, allowing the use of different bus voltages on each SCn/SDn pair such that 1.8-V, 2.5-V, or 3.3-V parts can communicate with 5-V parts. This is achieved by using external pull-up resistors to pull the bus up to the desired voltage for the master and each slave channel.

      1. Hallo,
        mir geht es um die erste Frage. ob die anderen I2C Teilnehmer getrennt/geschützt sind, wenn ein Teilnehmer einen Spannung- bzw. Stromschlag bekommt/verursacht. Hintergrund meiner Frage: ich habe früher im Berufsleben industrielle SPS verwendet und beim Umstieg auf eine Arduino-basierte SPS (Industrial Shields aus Spanien) kamen Probleme im Bezug auf den Schutz der einzelnen Eingänge etc. Einige Klemmen haben dann nicht mehr funktioniert. Ich bin unzufrieden und möchte selber eine stabile Lösung bauen, die gegen Stromschläge robust ist.
        Ich gebe hier wieder Bescheid, wenn ich durch das Datenblatt schlau werde. Vielleicht ist der Weg über I2C doch nicht der Richtige…

  6. Hi,

    ich schätze Deine Seite sehr, auch vielen Dank für die Anregungen durch diesen Beitrag!
    Das „merkwürdige Verhalten“ mit den MOSFETs kommt durch das einbringen von großen Kapazitäten in die SDA & SCL Leitungen zustande, wie im Datenblatt des IRF540 gut zu lesen. In Verbindung mit diesem PDF: https://www.ti.com/lit/an/slva689/slva689.pdf? wird es dann klar ersichtlich. Also am besten nur „kleine“ MOSFETs verwenden.

    Gruß André

    1. Vielen Dank nochmal – ich habe es mit kleineren MOSFETs probiert und es funktioniert wunderbar ohne zusätzliche Pull-Ups. Den Beitrag habe ich angepasst.

      1. Hi,

        mir ist klar, daß nicht jeder SOT-23 löten will, oder kann, aber ich würde die MOSFETs nehmen, die man auch für LevelShifter benutzt, BSS138. Diese funktionieren bis 2,5V garantiert. SOT-23 kann man problemlos auf Lochraster löten, selbst schon mehrfach gemacht.

        Gruß André

        1. Ist natürlich kompakter. Bezüglich der Kapazität nimmt sich das zumindest mit dem 2N7000 nicht viel. Danke für noch einen nützlichen Kommentar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert