Az I2C kommunikáció

Az I2C kommunikációhoz szükséges két vezetéket I2C busz-nak is szokták becézni. Azt hiszem valami akkor “busz” ha sokan vannak rajta. Ezen a két vezetéken bizony sokan is lehetnek egyszerre, tehát busz.
A buszrendszer konkrét működését nem is kell megismerni a használatához. Természetesen nem árt, de néhány apróság is elég. Ha részletesebb infóra van szükséged, kattints ide!
Az I2C buszrendszert azért találták ki, hogy sok-sok eszközt lehessen kevés kivezetés felhasználásával összekötni. Ehhez elegendő két kivezetés, melyeknek a neve SDA és SCL. Ezeket a jelöléseket minden eszközön megtaláljuk. Az Arduino Uno-on erre a az analóg bemenetekből kettőt kell feláldoznunk, az A5 és A4 bemeneteket, sőt még külön is kivezették a fenti jelöléssel:


Sok-sok eszközt (összesen 127db-ot) párhuzamosan lehet kötni. Természetesen minél több eszköz lóg a buszon, annál lassúbb lesz a kommunikáció, hiszen egyszerre a párhuzamos bekötés miatt mindig csak egy-egy eszköz kommunikálhat egymással. Az egyszerűbb áramköröknél (LCD kijelző, hőmérő chip, memória bővítő chip stb.) ez általában elegendő sebességet fog eredményezni. Műholdat amúgy sem akarunk irányítani. Legalább is mi kezdők!
A sok párhuzamosan kötött eszköz közül mindig egy határozza meg, hogy ki adhat vagy fogadhat adatokat. Ezt hívjuk master-nak. A master állítja elő az órajelet az SCL vezetékre. A többi kiszolgáló eszköz neve slave. Az SCL jelvezetéken minden eszköz megkapja az órajelet, az SDA vezetéken pedig az adat közlekedik. A mester először mindig megadja a slave címét, és az így kiválasztott slave felé küld, vagy onnan fogad adatot az SDA jelvezetékkel.

A kommunikáció során ismerni kell az eszköz címét. Ha ez nem derül ki a műszaki dokumentációból, akkor úgynevezett port scener programot célszerű használni. Ez sorban végigkérdezgeti az összes lehetséges címet az I2C buszon, és ha van ott valaki, akkor az válaszol, ha nincs akkor nem válaszol. Jó ez a szabványos működés. A program a soros monitoron tájékoztat az eredményről. Itt a forrása:

#include <Wire.h>
byte error, address;
int nDevices;

void setup()
{
  Wire.begin();               //I2C busz inicializálása
  Serial.begin(9600);    //soros port inicializálása
}
void loop()
{
  Serial.println("I2C kerses indul:"); //bemutatkozunk a soros porton
  nDevices = 0;
  for(address = 1; address < 127; address++ )
  {
    // Ez a program a Write.endTransmisstion függvényt használja,
    // hogy megnézze, van-e eszköz a vonalon. Ha nincs akkor hibával
    // tér vissza a függvény (error>0), ha a visszatérési érték 4 (error=4)
    // akkor van eszköz a vonalon, de ismeretlen hiba történt (ezt nem tudom, mikor 
    // fordulhat elő, még nem tpasztaltam)
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
    if (error == 0)
    {
      Serial.print("Eszköz címe:");
      Serial.print("  DEC:");
      Serial.print(address,DEC);
      Serial.print(" HEX:0x");  //a hexadecimális számokat 0x-el szoktuk kezdeni (jelölni)
      if (address<16) {Serial.print("0");} //vezető nulla, mert a hexa számok két szájeggyel néznek ki jól
      Serial.print(address,HEX);
      Serial.print("  BIN:0b");  //a bináris számokat 0b-vel szoktuk kezdeni (jelölni)
      Serial.println(address,BIN);
      nDevices++;
    }
    if (error == 4)
    {
      Serial.print("Isemertelen hiba:");
      Serial.print("  DEC:");
      Serial.print(address,DEC);
      Serial.print(" HEX:0x");  //a hexadecimális számokat 0x-el szoktuk kezdeni (jelölni)
      if (address<16) {Serial.print("0");} //vezető nulla, mert a hexa számok két szájeggyel néznek ki jól
      Serial.print(address,HEX);
      Serial.print("  BIN:0b");  //a bináris számokat 0b-vel szoktuk kezdeni (jelölni)
      Serial.println(address,BIN);
      nDevices++;
    }
  }
  if (nDevices == 0)
    Serial.println("Nincs eszkoz a buszon");
  else
    Serial.println("kereses vege!");
  delay(10000);           // 5 másodperc múlva újra elkezdünk keresni
}

Itt be is fejezhetnénk az I2C kommunikáció bevezető ismereteit, és rátérhetnénk egy konkrét példára. Ha érdekelnek még további finomságok, akkor olvass tovább, ha nem, akkor kattints ide, és meglátod, hogyan kell használni a karekteres LCD kijelzőt!

Az I2C busz sebessége
Az, hogy valami gyors, vagy éppen lassú, az nagyon relatív. Ha egy LCD kijelzőre kell 32 karakternyi információt kiírni, akkor ahhoz másodpercenként néhányszor száz karakter átvitele is elég gyors. Ha egy ramba kell több száz Kbyte infót átmozgatni, akkor az előbbi sebesség idegesítően lassú lesz. Az I2C busz meglehetősen sok értéktelen adatot is mozgat, hiszen az eszközöket címezni kell és meg kell azt is mondani a master-nek a számukra, hogy mit kell csinálni, és csak ez után jön az értékes adat. Azonban ennek eredménye a kényelem. Hogy mire is képes az I2C busz, az az órajel frekvenciájától függ. Elméletben a végtelenbe növelhetnénk a frekvenciát, de vannak határok. Pl. ott van a rendelkezésre álló órajel, ami a processzor órajele. Ez UNO esetén 16Mhz. Viszont a belső áramkörök miatt, ez nem lehet az I2C busz órajele, mindössze max. 1Mhz fér ki a csövön, azaz a SCL vezetéken.

A konkrét órajelet egy úgynevezett TWBR regiszter értékébe írt 0-255 közötti számmal lehet leosztani, azaz lassítani. Miért kéne lassítani? Azért mert minél nagyobb az órajel, annál érzékenyebb a rendszer a jelvezetékek hosszára. A hosszú vezetékeken több zavarjel, kéretlen impulzus gyűlik össze, és a hosszúsággal együtt nő a kapacitásnak nevezett elektromos paraméter értéke. A vezeték kapacitása hatással van a jelalakra, és a szép négyszög alakul jelből egyre lekerekítettebb, göbölyűbb jelalak lesz. Ez pedig elbizonytalanítja a jelek érzékelését. Egy idő után már a jelvezetéken terjedő jeleket a vevőegység nem képes felismerni, vagy hibás adatot érzékel. Ha tehát messzire kell elvinni az infót, akkor nincs mit tenni, csökkenteni kell a frekvenciát. A frekvencia csökkentésével a egyre hosszabbak lesznek azok az időszakok amikor a jel nem változik. A jelek változásakor (fel és lefutó élek) a jelalak továbbra is torz, de hagyunk időt, hogy beálljon a jelszint és a vevő csak ekkor olvassa le a jelvezeték feszültségét.

Az I2C busz frekvenciája alapértelmezetten 100Khz egy UNO esetén. A TWBR regiszter értékének változtatásával ezt a frekvenciát tudjuk növelni és csökkenteni. Az én gyakorlati tapasztalatom, hogy 100Khz frekvencia esetén kb 8 méteres (riasztó vezeték) kábellel még működött az átvitel. Nem vizsgáltam, hogy mi történik ha beindítjuk a porszívót, és sérül-e az átvitt infó. Szerintem igen, ezért ilyen hosszú vezetéket lehetőleg nem használnék. Amikor a kísérletet végeztem, még nem tudtam, hogy lehet növelni a frekvenciát, valamint hosszabb vezetékem sem volt, így csak ennyit tudok állítani: 8m kábelen még működik egy LCD kijelző az Arduino UNO-ról. A nagyon okosok szerint az 1Mhz órajel frekvencia esetén 10cm a maximális vezetékhossz. Ezt nem hittem el, és jól tettem, mert ennél azért jóval hosszabb is lehet. Példaként egy valós áramkörben egy BME280 szenzort kérdezgettem (3m vezeték) és egy LCD kijelző is működött (30cm vezeték), kipróbáltam a frekvencia állítgatását. 1Mhz frekin még működött az LCD. Egy alkalommal (4-5 próbálkozás) sikerült a BME280 lekérdezése, de legtöbbször nem. A frekvenciát egészen 500Khz-ig keltett csökkenteni, hogy minden működjön.

A frekvencia beállításához tudni kell, hogy a TWBR regiszter mely értékeihez, milyen órajel tartozik. Íme ehhez egy általam kreált táblázat néhány értékkel:

AdatVégrehajtási időFrekvencia
0387ms 1MHz
1543ms 888,888KHz
2571ms 800KHz
4611ms 666,666KHz
6660ms 571,428KHz
8709ms 500KHz
12 805ms 400KHz
16901ms 333,333KHz
321283ms 200KHz
642033ms 111,111KHz
722243ms 100KHz
1283551ms 58,823KHz
2556555ms 30,418KHz

A végrehajtási idő az alább található programra vonatkozik. A programot olyan hardveren futtattam, ahol az Arduino-t és az LCD kijelzőt kb. 20 cm-es vezetékek kötötték össze.
10×16 karaktert írtam ki az LCD kijelzőre. A 160 karakter átvitele 1 Mhz buszfrekvencián kb. 0,4 másodperc, míg 30,4Khz buszfrekvencián 6,5 másodperc. Miközben a program fut, „0” betűk rohangálnak a képernyőn. 30,4 Khz frekvenciánál szemmel jól követhető módon futnak végig az “0” betűk a kijelző első sorában. 1 Mhz esetén már semmit nem látni, csak halvány elmosódott pixeleket, mivel már meg sem tudja jeleníteni a rövid időre megjelenő betűt a kijelző. Az LCD fizikai működése ehhez lassú!

És ime a példa program 30,4 Khz buszfrekvencia beállításával. Sajnos ebben már használok LCD kijelzőt, amiről még csak a későbbiekben lesz szó (ha sorban haladsz az általam kijelölt úton). FONTOS!!! tapasztalati tudnivaló: A TWBR regiszter érték megadását nem lehetett a setup részbe beírni, mert ott nem reagált rá a program. Mintha akkor állítaná be a fordító a regiszter értékét a default 72-re, amikor a program futása a loop ciklushoz ér, és ez felülírja amit a setup-ban megadtunk. Kipróbáltam, hogy a loop-ban csak egyszer adtam ki az értékmegadást, így is működött. Azonban ehhez egy if-et kellett berakni a loop elejére, ami a processzornak nagyobb munka, mint egy értékmegadás. Így energiatakarékossági okokból feleslegesen minden loop elején beállítom az értéket. Legyünk zöldek, mentsük meg a világot!

Kezdőknek még annyit, hogy a processzor regisztereire közvetlenül lehet a nevükkel hivatkozni a programban. Ezt én is csak most tudtam meg ezen példa kapcsán. Pl. ha a programban a frekvencia kiírását végző sorban:   
Serial.println(F_CPU/(TWBR*2+16));
az F_CPU változó név a 16.000.000 értéket képvisel, ami az órajel frekvencia azUNO esetén. Sok ilyen változó illetve regiszter van, biztosan óvatosnak kell lenni velük!

#include <Wire.h>    //I2C library
#include <LiquidCrystal_I2C.h>  //I2C LCD kezelő könyvtár         
LiquidCrystal_I2C lcd(0x3F, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE); //LCD paraméterek megadása, a 4 soros LCD-m címe 0x3F, a 2 soros 0x27 
int ido=0;

void setup() 
{
  Serial.begin(9600);
  Wire.begin();          // I2C busz használat indítása
  lcd.begin(20,4);       //LCD inicializálása
  lcd.backlight();       //háttérvilágítás bekapcsolása
  lcd.clear();
}

void loop()
{
  ido=millis();
  //TWBR = ((F_CPU / frekvencia) - 16) / 2;
  TWBR = 255;
  for (int i=0;i<10;i++)
  {
    lcd.setCursor(0,0);lcd.print("0              ");
    lcd.setCursor(0,0);lcd.print(" 0             ");
    lcd.setCursor(0,0);lcd.print("  0            ");
    lcd.setCursor(0,0);lcd.print("   0           ");
    lcd.setCursor(0,0);lcd.print("    0          ");
    lcd.setCursor(0,0);lcd.print("     0         ");
    lcd.setCursor(0,0);lcd.print("      0        ");
    lcd.setCursor(0,0);lcd.print("       0       ");
    lcd.setCursor(0,0);lcd.print("        0      ");
    lcd.setCursor(0,0);lcd.print("         0     ");
    lcd.setCursor(0,0);lcd.print("          0    ");
    lcd.setCursor(0,0);lcd.print("           0   ");
    lcd.setCursor(0,0);lcd.print("            0  ");
    lcd.setCursor(0,0);lcd.print("             0 ");
    lcd.setCursor(0,0);lcd.print("              0");
 }
  Serial.print("TWBR eteke:");
  Serial.println(TWBR);
  Serial.print("I2C orajel ferkvencia:");
  Serial.println(F_CPU/(TWBR*2+16));
  Serial.print("Vegrehajtasi ido:");
  Serial.println(millis()-ido);
  Serial.println("");
}

Még egy fontos tanulság a programból. Az int típusú változó értékhatára kisebb mint a millis() függvény által visszaadott szám, ami unsigned long típusú. Ha elegendő ideig várunk (egészen pontosan 64.000 milisecundum-ig), akkor furcsaságot fogunk tapasztalani, ugyanis a mért időt hibásan jelzi ki. Ezt azzal lehet kivédeni, hogy a program 4. sorában “int ido“; helyett “unsigned long“-ot írunk. Én most itt direkt hagytam hibásan, hogy okuljunk belőle! Így is el lehet cseszni egy programot! A net-ről beszerzett programokban sok ilyen hibát találtam. Egyes programok nagyon-nagyon sok helyen fellelhetők, mert különböző cikkekben idézgetik. Láthatóan senki nem veszi a fáradságot, hogy ténylegesen ki is próbálja ezeket. Nos, az én programjaimban is lehetnek ilyen hibák.

Van a frekvencia beállításra más lehetőség is. A Wire program könyvtárnak van ugyanis egy frekvencia beállító függvénye:
Wire.setClock(frekvencia)
Ebben a függvényben közvetlenül beírhatunk egy frekvencia értéket. A program valamely közeli frekvenciát fogja beállítani. Pl. én beírtam a 30.500-at. A valós beállított frekvencia 30.534 lett, ekkor a TWBR értéke 254. Vigyázat, 30.418 alatti értéknél 800.000Khz frekvenciát állít be, nem jól működik. Nekem ezért a TWBR-el történő beállítás szimpatikusabb.

Most, hogy ennyi mindent megtudtunk az I2C kommunikációról, érdemes lenne a gyakorlatban is használni. Mi sem egyszerűbb ennél. Be kell szerezni egy 2 soros, soronként 16 karakteres LCD kijelzőt (kb. 1000Ft). Önmagában a kijelző nem I2C kommunikációval működik (de lehetne Arduino-val használni, csak sok kivezetést kellene felhasználni), kell még hozzá egy LCD meghajtó áramkör. Ezt kifejezetten a kijelző meghajtására készítették, nagyon le fogja egyszerűsíteni az életünket. Ha szeretnéd látni hogyan, akkor kattints ide!

Mennyire volt hasznos amit olvastál? Értékelés után szövegesen is leírhatod megjegyzéseidet és véleményedet!

Kattints egy csillagra az értékeléshez!

Szövegesen is leírhatod véleményedet! Ha kérdésed van, ne felejtsd el megadni az email címedet!