poniedziałek, 25 marca 2024

ESP32 i I2C czyli czym połączyć elektroniczne klocki LEGO


Budowa nowych, nawet bardzo złożonych funkcjonalnie, układów elektronicznych może być bardzo prosta i przyjemna.  Pod warunkiem, że twórczo wykorzystamy to co jest dostępne na rynku w formie gotowych modułów.  A jest tego sporo dzięki pracowitym Chińczykom i w śmiesznie niskich cenach. Sercem takiego projektu musi być oczywiście jakiś mikrokontroler ale z tym jest najmniejszy kłopot od czasu pojawienia się ARDUINO.  Wszystkie  te elementy trzeba jednak jakoś inteligentnie z sobą połączyć. Dziś o tym jak to zrobić bez zbytniego wysiłku...




Patrząc na każdy mikroprocesor widzimy porty, porty i jeszcze raz porty. Większość z nich to jednobitowe piny cyfrowe lub piny analogowe. I to w zasadzie cała elektronika. We wszystkich układach mamy albo dwustanowy sygnał binarny albo liniowy sygnał analogowy. Ale o ile sygnał analogowy niesie ze sobą informacje związaną jedynie z jego wartością to możliwości sygnałów cyfrowych okazały się dużo większe. Paradoksalnie. Mając jedynie dwa stany do przekazania większej ilości informacji musiał pojawić się dodatkowy element - czas. I tak narodził się protokół czyli uporządkowana zmiana sygnału binarnego w czasie.  

I choć dzisiaj w mikrokontrolerach wciąż przeważają porty cyfrowe lub analogowe, do których można podłączyć proste sygnały, to dominującą funkcją stanowiącą o sile rynkowej danego układu staje się umiejętność obsłużenia jak największej liczby protokołów komunikacyjnych. Znakomitym przykładem jest tu historia ESP8266. Procesor jakich tysiące innych zagarnął potężną część rynku tylko jednym - ma  sprzętowo zaimplementowany protokół WiFi w nieprzyzwoicie niskiej cenie .  

Dziś patrząc poważnie na mikrokontroler powinniśmy widzieć nie ilość dostępnych portów cyfrowych czy analogowych a ilość i rodzaj sprzętowo obsługiwanych  protokołów komunikacyjnych.

Standardowo większość procesorów dla amatorskiej elektroniki obsługuje następujące ich rodzaje:

  • I2C - wolny protokół komunikacyjnych krótkiego zasięgu danych cyfrowych
  • I2S - cyfrowy protokół danych analogowych (audio)
  • SPI - szybszy (od I2C) protokół komunikacyjny
  • UART - 5V lub 3V3 odpowiednik protokołu RS232

Lepsze modele procesorów dla hobbystów posiadają dodatkowo

  • WiFi 802.11 - (w ESP to standard) protokół komunikacji radiowej
  • Bluetooth - protokół komunikacji radiowej krótkiego zasięgu
  • Ethernet - komunikacja przewodowa LAN
  • SDIO - protokół dla kart pamięci (dawniej SD)
  • IR - komunikacja w podczerwieni
Oprócz rodzajów protokołów komunikacyjnych ważna jest również ich liczba danego rodzaju. Taki np ESP32 ma 3 UARTy 3 SPI i 2 I2C.

W moich projektach do tej pory wykorzystywałem jedynie WiFi i UART. WiFi dzięki procesorom ESP pozwolił na stworzenie sieci IoT obejmującej całą nieruchomość. UART to przede wszystkim prostota wgrywania nowych projektów do mikrokontrolerów i niezwykła łatwość podglądania tego co dzieje się w programie. Konfiguracja UART, dzięki ARDUINO, jest uproszczona do maksimum. Jedynie co musimy zgrać to prędkości transmisji procesora i komputera. Z WiFi jest trochę trudniej ale i tu większość ustawień załatwia za nas ARDUINO.
Teraz przyszedł czas na protokoły bliskiego zasięgu tj. I2C i SPI. Bardzo bliskiego gdyż mają połączyć ESP z oddalonymi o parę centymetrów gotowymi modułami. Jakimi - to nie jest jeszcze do końca wiadome.
Ale z roku na rok rośnie liczba tanich modułów z zaimplementowanymi protokołami I2C i SPI dzięki rozwojowi telefonów komórkowych tabletów czy zegarków, dla których projektowane są te elementy.
Co chwila pojawiają się nowe moduły więc warto przeglądać aktualne oferty takich sklepów jak botland  czy kamami gdzie takie układy (głównie czujniki) są ładnie pogrupowane i opisane.

Ok mamy już moduł z I2C i co dalej?
Dalej jest już bardzo prosto. Dużo łatwiej niż łączenie za pomocą UARTa. 
Po pierwsze wszystkie moduły I2C łączymy równolegle do tych samych szyn SCL i SDA. 
Po drugie - nie da się uszkodzić ani modułów ani procesora omyłkowym połączeniem (no może poza zamianą masy i zasilania). Linie sygnałowe są typu OC (open collector) więc co najwyżej układ nie będzie odpowiadał na zapytania serwera. 
Po trzecie jedyny parametr jaki musimy znać by uruchomić łączność w ramach I2C to adres urządzenia abonenckiego. Adres ten jest na sztywno wpisany w kość obsługującą protokół choć czasami jest możliwość jego niewielkiej modyfikacji. Adresów w I2C jest tylko 127 (w podstawowym standardzie)i może się zdarzyć, że dwa moduły mają identyczne adresy. Tak będzie na pewno gdy zachcemy używać dwu identycznych modułów np. wyświetlaczy. Jeśli moduł ma taką możliwość dodatkowym pinem można zmienić adres na inny niż domyślny. Są też moduły posiadające dodatkową linię CS  pozwalającą wybrać konkretny moduł. Niektóre moduły mogą wymagać restetu dla prawidłowej pracy -  czyli jeszcze jedna linia dodatkowa. Uff a miał to być taki prosty układ łączności szeregowej. 
I prawdę powiedziawszy takowy jest. Te dodatkowe linie są potrzebne tylko w wyjątkowych przypadkach. I2C to naprawdę świetny sposób komunikacji między procesorem a gotowymi modułami instalowanymi na płytce naszego projektu lub nawet w pewnej (niedalekiej do 1-2 m) odległości od niego. 
Schemat połączenia modułu I2C do mikroprocesora teoretycznie wygląda tak


Teoretycznie gdyż rezystory podciągające możemy sobie darować gdy używamy gotowych modułów I2C. Większość (jeśli nie wszystkie) takie rezystory mają już zainstalowane w w sobie. 

Do obsługi komunikacji I2C stosujemy bibliotekę wire.h dostępną  w ARDUINO IDE. Przypisuje ona domyślne porty procesora jako SDA i SCL. Które? Te które zobaczyć można na większości schematów typu pinout - dla ESP8266  GPIO 4 to linia SDA a GPO5 to SCL.



Wystarczy więc procedura
 Wire.begin();
by móc łączyć się z modułem obsługującym I2C. Gdy nie pasują nam w projekcie domyślne wartości w ESP możemy dowolnie wskazać porty, które będą odpowiedzialne za transmisję I2C. W tym celu lekko zmodyfikujemy wywołanie biblioteki WIRE 
 
 Wire.begin( x , y );

gdzie x to port linii SDA a y linii SCL

W procesorach posiadających więcej niż jeden I2C (ESP32)  jednoczesne korzystanie z obu kanałów transmisji wymaga innej deklaracji portów

#include <Wire.h>
#define SDA_0 21
#define SCL_0 22
 
#define SDA_1 18
#define SCL_1 19
 
TwoWire I2C_0 = TwoWire(0);
TwoWire I2C_1 = TwoWire(1);
 
void setup()
{
  I2C_0.begin(SDA_0 , SCL_0 , I2C_Freq );
  I2C_1.begin(SDA_1 , SCL_1 , I2C_Freq );
}

Jak widać w powyższych przykładach nie ma tu żadnej informacji jak ustawić adres urządzenia abonenckiego podłączonego do szyny I2C. Taką informację zawierać będzie biblioteka dla danego modułu. 

Ale możemy ją również uzyskać skanując porty procesora pod kątem przyłączonych do niego modułów I2C. Szkic takiego skanera można ściągnąć tu


#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");
}

Co zrobić z tą wiedzą podpowie nam nie za długo nasz ulubiony ciąg dalszy.


Brak komentarzy:

Prześlij komentarz