Comunicația I2C pentru începători
Cum funcționează magistrala I2C (I²C) și un exemplu simplu de mini-librărie pentru Arduino (C/C++).
Ce este I2C?
I2C (Inter-Integrated Circuit), scris uneori I²C, este un protocol serial sincron, pe 2 fire, folosit foarte des
pentru a conecta un microcontroller la senzori, memorii EEPROM, RTC-uri, convertoare ADC/DAC și alte periferice.
Avantajul principal este că poți conecta mai multe dispozitive pe aceleași două linii, fără să consumi mulți pini.

1) Firele I2C și nivelurile electrice
Magistrala I2C folosește două linii:
- SDA (Serial Data) – linia de date
- SCL (Serial Clock) – linia de ceas
Ambele linii sunt de tip open-drain/open-collector. Asta înseamnă că dispozitivele nu “urcă” linia la 1 logic,
ci doar o pot trage la 0 logic. Nivelul 1 se obține prin rezistențe pull-up către VCC (de exemplu 3.3V sau 5V).
- Fără rezistențe pull-up, magistrala nu funcționează corect.
- Ai grijă la tensiuni: un senzor 3.3V pe un bus 5V poate necesita level-shifter (dacă nu e 5V tolerant).
Pini I2C (exemple uzuale)
- Arduino UNO/Nano: SDA = A4, SCL = A5
- Arduino Mega: SDA = 20, SCL = 21
- ESP32: pinii pot fi configurați (depinde de board și librăria folosită)
2) Master și Slave
În I2C, dispozitivul master inițiază comunicația: generează ceasul (SCL) și pornește tranzacțiile.
Dispozitivele slave răspund atunci când sunt adresate.
Teoretic pot exista mai mulți masteri pe aceeași magistrală (multi-master), dar în proiectele Arduino uzuale
ai aproape întotdeauna un singur master.
3) Adresarea (7-bit vs 10-bit)
Cel mai des se folosește adresarea pe 7 biți (0…127). Unele datasheet-uri prezintă și “adresa pe 8 biți”,
unde bitul cel mai puțin semnificativ (LSB) reprezintă direcția R/W (0 = write, 1 = read).
Wire, folosești adresa pe 7 biți (de exemplu 0x68),nu varianta shiftată (
0xD0/0xD1).4) START, STOP, ACK/NACK și tranzacțiile I2C
O tranzacție I2C tipică are următoarele etape:
- START: SDA trece din 1 în 0 în timp ce SCL este 1.
- Address + R/W: master trimite adresa (7 biți) + bitul de direcție (0 = write, 1 = read).
- ACK/NACK: după fiecare byte transmis, receptorul confirmă cu ACK (trage SDA la 0 pe al 9-lea puls).
- Date: se trimit unul sau mai mulți bytes.
- STOP: SDA trece din 0 în 1 în timp ce SCL este 1.
Un model foarte comun (pentru citirea unui registru dintr-un senzor) este:
Write pentru a seta pointerul de registru, apoi Repeated START și Read
pentru a citi N bytes.
5) Viteze standard
- Standard mode: 100 kHz
- Fast mode: 400 kHz
Viteza maximă stabilă depinde de calitatea cablajului, pull-up, lungimea firelor, numărul de dispozitive și
capacitatea totală a magistralei.
6) Probleme uzuale (și simptome)
- Pull-up lipsă / valori nepotrivite: semnale lente, erori intermitente, bus blocat.
- Adresă greșită (7-bit vs 8-bit): dispozitivul nu răspunde (NACK).
- Un dispozitiv ține SDA low (bus locked): cabluri, reset, glitch; uneori necesită recovery.
- Tensiuni incompatibile: comportament instabil sau risc de avarie.
Exemplu: mini-librărie I2C pentru Arduino (C/C++)
Exemplul de mai jos este o “mini-librărie” simplă care permite:
inițializare I2C, scriere într-un registru (8-bit), citire dintr-un registru (8-bit) și citire de N bytes consecutiv.
Structură fișiere
SimpleI2C.hSimpleI2C.cppexample.ino
SimpleI2C.h
#pragma once
#include <Arduino.h>
#include <Wire.h>
class SimpleI2C {
public:
explicit SimpleI2C(uint8_t addr7);
void begin(uint32_t clockHz = 100000);
bool writeReg8(uint8_t reg, uint8_t value);
bool readReg8(uint8_t reg, uint8_t &value);
bool readBytes(uint8_t startReg, uint8_t *buf, size_t len);
private:
uint8_t _addr;
bool writeBuffer(const uint8_t *data, size_t len);
};
SimpleI2C.cpp
#include "SimpleI2C.h"
SimpleI2C::SimpleI2C(uint8_t addr7) : _addr(addr7) {}
void SimpleI2C::begin(uint32_t clockHz) {
Wire.begin();
Wire.setClock(clockHz);
}
bool SimpleI2C::writeBuffer(const uint8_t *data, size_t len) {
Wire.beginTransmission(_addr);
for (size_t i = 0; i < len; i++) {
Wire.write(data[i]);
}
// endTransmission() returnează 0 la succes, altfel un cod de eroare
uint8_t err = Wire.endTransmission(true); // true = send STOP
return (err == 0);
}
bool SimpleI2C::writeReg8(uint8_t reg, uint8_t value) {
uint8_t payload[2] = {reg, value};
return writeBuffer(payload, sizeof(payload));
}
bool SimpleI2C::readReg8(uint8_t reg, uint8_t &value) {
// 1) set pointer la registru (write fără STOP => repeated START)
Wire.beginTransmission(_addr);
Wire.write(reg);
uint8_t err = Wire.endTransmission(false); // false = no STOP
if (err != 0) return false;
// 2) cere 1 byte
uint8_t got = Wire.requestFrom((int)_addr, 1, (int)true); // true = STOP la final
if (got != 1) return false;
value = Wire.read();
return true;
}
bool SimpleI2C::readBytes(uint8_t startReg, uint8_t *buf, size_t len) {
if (buf == nullptr || len == 0) return false;
// Set register pointer
Wire.beginTransmission(_addr);
Wire.write(startReg);
uint8_t err = Wire.endTransmission(false); // repeated START
if (err != 0) return false;
// Request len bytes
size_t got = Wire.requestFrom((int)_addr, (int)len, (int)true);
if (got != len) return false;
for (size_t i = 0; i < len; i++) {
buf[i] = Wire.read();
}
return true;
}
example.ino
Înlocuiește adresa și registrele cu cele din datasheet-ul dispozitivului tău. Arduino Wire folosește adresă 7-bit.
#include "SimpleI2C.h"
// Exemplu: un device la adresa 0x68 (adresa 7-bit!)
SimpleI2C dev(0x68);
void setup() {
Serial.begin(115200);
dev.begin(100000); // 100kHz
// Exemplu: scrie 0x01 în registrul 0x10
if (!dev.writeReg8(0x10, 0x01)) {
Serial.println("I2C write failed");
}
}
void loop() {
uint8_t val = 0;
if (dev.readReg8(0x11, val)) {
Serial.print("Reg 0x11 = 0x");
if (val < 16) Serial.print('0');
Serial.println(val, HEX);
} else {
Serial.println("I2C read failed");
}
delay(500);
}
Note practice
- Dacă ai erori intermitente: scurtează firele, verifică GND comun, scade viteza la 100 kHz și verifică pull-up (4.7k–10k tipic).
- Dacă device-ul nu răspunde: rulează un I2C scanner ca să confirmi adresa 7-bit.
- Pentru mulți senzori, secvența “write register, repeated start, read N bytes” este esențială.
Exemplu I2C pentru ESP32 (Arduino Core)
Conține: mini-librărie I2C cu pini SDA/SCL configurabili (ESP32), funcții de citire/scriere registre și un I2C scanner
pentru identificarea adreselor dispozitivelor de pe magistrală.
Observații practice pentru ESP32
- Tensiune: ESP32 lucrează la 3.3V (în general nu e 5V tolerant). Pull-up-urile trebuie la 3.3V.
- Pini: SDA/SCL sunt configurabili. Uzual: SDA=GPIO21, SCL=GPIO22 (depinde de placă).
- Viteză: 100 kHz este cea mai sigură. 400 kHz poate funcționa cu fire scurte și pull-up potrivit.
Mini-librărie: SimpleI2C (ESP32)
Librăria folosește TwoWire astfel încât să poți selecta Wire sau Wire1.
API-ul permite setarea pinilor SDA/SCL și a frecvenței direct în begin().
Structură fișiere
SimpleI2C.hSimpleI2C.cppexample.inoi2c_scanner.ino(opțional)
SimpleI2C.h
#pragma once
#include <Arduino.h>
#include <Wire.h>
class SimpleI2C {
public:
explicit SimpleI2C(uint8_t addr7, TwoWire &bus = Wire);
// ESP32: poți seta pinii SDA/SCL + frecvența
bool begin(int sdaPin = 21, int sclPin = 22, uint32_t clockHz = 100000);
bool writeReg8(uint8_t reg, uint8_t value);
bool readReg8(uint8_t reg, uint8_t &value);
bool readBytes(uint8_t startReg, uint8_t *buf, size_t len);
private:
uint8_t _addr;
TwoWire *_bus;
bool writeBuffer(const uint8_t *data, size_t len, bool sendStop = true);
};
SimpleI2C.cpp
#include "SimpleI2C.h"
SimpleI2C::SimpleI2C(uint8_t addr7, TwoWire &bus) : _addr(addr7), _bus(&bus) {}
bool SimpleI2C::begin(int sdaPin, int sclPin, uint32_t clockHz) {
// ESP32 Arduino: begin(sda, scl, freq) este suportat.
// Fallback inclus pentru compatibilitate.
bool ok = _bus->begin(sdaPin, sclPin, clockHz);
if (!ok) {
_bus->begin(sdaPin, sclPin);
_bus->setClock(clockHz);
ok = true;
}
return ok;
}
bool SimpleI2C::writeBuffer(const uint8_t *data, size_t len, bool sendStop) {
_bus->beginTransmission(_addr);
for (size_t i = 0; i < len; i++) {
_bus->write(data[i]);
}
// endTransmission() returnează 0 la succes, altfel cod de eroare
uint8_t err = _bus->endTransmission(sendStop);
return (err == 0);
}
bool SimpleI2C::writeReg8(uint8_t reg, uint8_t value) {
uint8_t payload[2] = {reg, value};
return writeBuffer(payload, sizeof(payload), true);
}
bool SimpleI2C::readReg8(uint8_t reg, uint8_t &value) {
// 1) Set pointer la registru (fără STOP => repeated START)
_bus->beginTransmission(_addr);
_bus->write(reg);
uint8_t err = _bus->endTransmission(false); // false = no STOP
if (err != 0) return false;
// 2) Cere 1 byte
uint8_t got = _bus->requestFrom((int)_addr, 1, (int)true); // STOP la final
if (got != 1) return false;
value = _bus->read();
return true;
}
bool SimpleI2C::readBytes(uint8_t startReg, uint8_t *buf, size_t len) {
if (!buf || len == 0) return false;
// Set register pointer
_bus->beginTransmission(_addr);
_bus->write(startReg);
uint8_t err = _bus->endTransmission(false);
if (err != 0) return false;
// Request len bytes
size_t got = _bus->requestFrom((int)_addr, (int)len, (int)true);
if (got != len) return false;
for (size_t i = 0; i < len; i++) {
buf[i] = _bus->read();
}
return true;
}
example.ino
Înlocuiește adresa și registrele cu cele din datasheet-ul dispozitivului tău. Arduino Wire folosește adresă 7-bit.
#include "SimpleI2C.h"
// Exemplu: dispozitiv la adresa 0x68 (7-bit)
SimpleI2C dev(0x68);
void setup() {
Serial.begin(115200);
// Dacă placa ta folosește alți pini, schimbă aici:
// dev.begin(SDA, SCL, frecventa);
dev.begin(21, 22, 100000);
// Exemplu: scrie 0x01 în registrul 0x10
if (!dev.writeReg8(0x10, 0x01)) {
Serial.println("I2C write failed");
}
}
void loop() {
uint8_t v = 0;
if (dev.readReg8(0x11, v)) {
Serial.printf("Reg 0x11 = 0x%02X\n", v);
} else {
Serial.println("I2C read failed");
}
delay(500);
}
I2C Scanner pentru ESP32 (recomandat)
Dacă nu ești sigur de adresa dispozitivului sau suspectezi o problemă de cablare, rulează un scanner.
Când găsește un device, va afișa adresa în format hex.
#include <Wire.h>
void setup() {
Serial.begin(115200);
// setează pinii reali folosiți
Wire.begin(21, 22, 100000);
Serial.println("Scanning I2C...");
for (uint8_t addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
uint8_t err = Wire.endTransmission(true);
if (err == 0) {
Serial.printf("Found device at 0x%02X\n", addr);
}
}
}
void loop() {}
și încearcă 100 kHz (nu 400 kHz).
Checklist pentru debug
- Ai GND comun între ESP32 și dispozitiv?
- Ai pull-up la 3.3V pe SDA și SCL (4.7kΩ recomandat)?
- Ai pinii corecți (SDA/SCL) setați în
Wire.begin()? - Ai adresă 7-bit corectă?
- Firele sunt suficient de scurte pentru viteza aleasă?
