I2C na dowolnych pinach NodeMCU / ESP8266

Tworząc projekty oparte o mikrokontrolery często komunikujemy się z urządzeniami podłączonymi przez magistralę I2C. W tym artykule zaprezentuję Wam jak komunikować się z NodeMCU przez I2C na dowolnej parze jego pinów.

Na Arduino komunikacja I2C to żaden problem – piny SDA i SCL są podpisane na płytce. Jeżeli jednak weźmiemy do ręki NodeMCU to zauważymy, że nie ma tam nigdzie takich oznaczeń. Szybko więc wklepujemy w google frazę „nodemcu i2c pins” po czym ten wypluwa nam np. taki wynik:

Radośnie więc podłączamy nasz układ zgodnie ze schematem, wywołujemy Wire.begin(); i wszystko działa :)

Co jeżeli jednak projektujemy PCB, gdzie głównym procesorem naszego urządzenia będzie NodeMCU i okaże się, że fajnie by było jakby jednak te D2 i D1 były w innym miejscu, bo nie jesteśmy w stanie odpowiednio poprowadzić ścieżek? Ewentualnie chcemy podłączyć dwa urządzenia, które mają taki sam na stałe ustawiony adres? Albo poprostu mamy taki kaprys. Odpowiedź jest prosta – wystarczy zajrzeć do pliku nagłówkowego biblioteki Wire dla NodeMCU.

Znajdziemy tutaj definicję metody begin:

void begin();

Jest to standardowa metoda, która uruchomi interfejs I2C na domyślnych pinach D2 oraz D1. Kilka linijek wyżej mamy jednak sparametryzowaną wersję tej metody:

void begin(int sda, int scl);

Bingo! Wystarczy, że wywołamy metodę begin podając jako pierwszy argument numer pinu danych, a drugi numer pinu zegara.

Należy jednak pamiętać, że nie mimo wszystko interfejs I2C mamy w procesorze tylko jeden. Jedyne co możemy zrobić to wybrać na których pinach zostanie on wyprowadzony. Konsekwencją takiej konstrukcji jest to, że nie możemy jednocześnie używać I2C na pinach np. D2, D1 oraz D6, D7. Jeżeli więc chcemy uzywać dwóch urządzeń na dwóch różnych parach pinów I2C musimy je nieustannie przełączać za pomocą Wire.begin(int sca, int scl).

Układ testowy

Żeby przetestować jak to działa możemy podłączyć wszystkie komponenty na I2C, które mamy pod ręką i podłączyć ję do różnych par pinów ;) Ważne, żeby te pary się nie nachodziły, czyli jeżeli w jednej parze użyjemy ponów D6 i D7 to już w drugiej nie możemy użyć żadnego z nich.

Ja zbudowałem sobie układ jak na poniższym zdjęciu (ekran jest na 5V, ale w tym przypadku nie ma to znaczenia, bo chcemy tylko żeby jego ekspander zgłosił się pod swoim adresem 0x27).

Skanowanie wszystkich portów

Na stronie Arduino Playground znajdziemy szkic do skanowania urządzeń podłączonych do portu I2C Arduino. Zadziała on także bez żadnego problemu na NodeMCU jeżeli nasze komponenty będą podłączone do domyślnych pinó D2 i D1. My jednak dobrze wiemy, że tak nie jest, więc musimy sobie delikatnie zmodyfikować szkic. Pierwszą modyfikacją jest zmiana nazwy funkcji loop() na scan() i utworzenie nowej, pustej funkcji loop. Następnie tworzymy globalną tablicę z parami pinów do przeskanowanie oraz funkcję scanAll(), gdzie przeskanujemy sobie wszystkie porty.

uint8_t i2cPins[][2] = {
  {D2, D1}, // Standardowe piny I2C
  {D5, D6}
};
void scanAll()
{
  for (int x = 0; x < sizeof(i2cPins) / (sizeof(uint8_t) * 2); x++) {
    Serial.print("I2C na pinach SDA: ");
    Serial.print(i2cPins[x][0]);
    Serial.print(",SCL: ");
    Serial.println(i2cPins[x][1]);
    Wire.begin(i2cPins[x][0], i2cPins[x][1]);
    scan();
  }
}

Zaintrygować może warunek pętli for:

sizeof(i2cPins) / (sizeof(uint8_t) * 2)

Chodzi o to, żeby przeskoczyć po wszystkich elementach tablicy. Dzielimy więc rozmiar tablicy w bajtach przez rozmiar pojedynczej zmiennej typu uint8_t pomnożony przez 2 (dlatego, że tablica jest dwuwymiarowa, a drugi wymiar ma właśnie rozmiar 2).

Cały szkic powinien więc wyglądać tak:

#include <Wire.h>

uint8_t i2cPins[][2] = {
  {D2, D1}, // Standardowe piny I2C
  {D5, D6}
};

void setup()
{
  Wire.begin();

  Serial.begin(9600);
  while (!Serial);             // Leonardo: wait for serial monitor
  Serial.println("\nI2C Scanner");
}

void loop()
{
  scanAll();
  delay(1000);
}

void scanAll()
{
  for (int x = 0; x < sizeof(i2cPins) / (sizeof(uint8_t) * 2); x++) {
    Serial.print("I2C na pinach SDA: ");
    Serial.print(i2cPins[x][0]);
    Serial.print(",SCL: ");
    Serial.println(i2cPins[x][1]);
    Wire.begin(i2cPins[x][0], i2cPins[x][1]);
    scan();
  }
}

void scan()
{
  byte error, address;
  int nDevices;

  Serial.println("Scanning...");

  nDevices = 0;
  for (address = 1; address < 127; address++ )
  {
    // The i2c_scanner uses the return value of
    // the Write.endTransmisstion to see if
    // a device did acknowledge to the address.
    Wire.beginTransmission(address);
    error = Wire.endTransmission();

    if (error == 0)
    {
      Serial.print("I2C device found at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.print(address, HEX);
      Serial.println("  !");

      nDevices++;
    }
    else if (error == 4)
    {
      Serial.print("Unknown error at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.println(address, HEX);
    }
  }
  if (nDevices == 0)
    Serial.println("No I2C devices found\n");
  else
    Serial.println("done\n");
}

Teraz możemy wgrać szkic i zobaczyć, że to naprawdę działa ;)

Numery pinów wyświetlane w monitorze portu są inne niż D2, D1 i D5, D6 dlatego, że są to prawdziwe numery GPIO w procesorze. D0,D1,D2 itd. są tylko stałymi, które odpowiadają wartością na PCB NodeMCU.