Hướng dẫn giao tiếp I2C trên ESP32: quét địa chỉ, đổi chân SDA/SCL và nối nhiều thiết bị

Hướng dẫn dùng bus I2C trên ESP32 với Arduino IDE: quét địa chỉ thiết bị, đổi chân SDA và SCL, nối màn hình OLED và ADS1115 chung một bus, xử lý khi hai thiết bị trùng địa chỉ.

Màn hình OLED, LCD có mạch I2C, module ADC ADS1115 và rất nhiều cảm biến đều giao tiếp với vi điều khiển qua bus I2C. Hiểu bus này giúp bạn nối được nhiều module chỉ với hai chân GPIO và tự tìm ra nguyên nhân khi chương trình báo không thấy thiết bị. Bài viết hướng dẫn quét địa chỉ I2C bằng ESP32, đổi chân SDA và SCL, nối nhiều thiết bị trên một bus và xử lý trường hợp trùng địa chỉ trên Arduino IDE.

1. I2C là gì?

I2C là chuẩn giao tiếp nối tiếp dùng hai dây tín hiệu: SDA truyền dữ liệu và SCL truyền xung clock. Một thiết bị chủ (ở đây là ESP32) phát xung clock và điều khiển việc truyền; các thiết bị tớ (màn hình, cảm biến) chỉ trả lời khi được gọi.

Sơ đồ bus I2C: ESP32 là thiết bị chủ, màn hình OLED, LCD I2C và ADS1115 nối chung hai dây SDA, SCL, mỗi thiết bị một địa chỉ
Nhiều thiết bị nối chung hai dây SDA và SCL; mỗi thiết bị có một địa chỉ riêng (hình vẽ minh họa).
  • Địa chỉ: mỗi thiết bị tớ có một địa chỉ 7 bit, thường viết ở dạng thập lục phân như 0x3C. Khoảng dùng được là 0x08 đến 0x77.
  • Điện trở kéo lên: hai dây SDA và SCL cần điện trở kéo lên nguồn. Hầu hết module bán sẵn đã hàn hai điện trở này trên mạch.
  • Tốc độ: mặc định 100 kHz, đủ cho màn hình và cảm biến thông thường. Nhiều thiết bị hỗ trợ 400 kHz.
  • Khoảng cách: I2C dành cho các linh kiện đặt gần nhau, dây nối nên dưới vài chục cm.

2. I2C trên ESP32

  • Chân I2C mặc định của ESP32 DevKit V1 là GPIO21 (SDA) và GPIO22 (SCL), trên mạch in là D21 và D22.
  • ESP32 cho phép đổi SDA và SCL sang hầu hết các chân GPIO khác (mục 6).
  • ESP32 có hai bộ điều khiển I2C độc lập, trong Arduino IDE là Wire và Wire1 (mục 8).
  • GPIO của ESP32 làm việc ở 3,3 V và không chịu được 5 V. Nên cấp nguồn 3,3 V cho các module I2C để bus cùng mức điện áp với ESP32.

3. Linh kiện cần chuẩn bị

4. Chương trình quét địa chỉ I2C

Khi chưa biết địa chỉ của một module, hoặc khi thư viện báo không tìm thấy thiết bị, việc đầu tiên nên làm là quét bus. Nối module theo bốn dây: nguồn vào 3V3, GND vào GND, SCL vào D22, SDA vào D21, rồi nạp chương trình sau (không cần cài thêm thư viện):

/*
  PYWORLD.VN - ESP32 quét địa chỉ các thiết bị trên bus I2C
  SDA = GPIO21, SCL = GPIO22 (chân I2C mặc định của ESP32 DevKit V1)
  Không cần cài thêm thư viện
*/

#include <Wire.h>

#define SDA_PIN 21
#define SCL_PIN 22

void setup() {
  Serial.begin(115200);
  delay(500);

  Wire.begin(SDA_PIN, SCL_PIN);
  Serial.println("\nQuet bus I2C...");
}

void loop() {
  int soThietBi = 0;

  // Địa chỉ I2C 7 bit dùng được nằm trong khoảng 0x08 đến 0x77
  for (uint8_t diaChi = 0x08; diaChi <= 0x77; diaChi++) {
    Wire.beginTransmission(diaChi);
    uint8_t ketQua = Wire.endTransmission();

    if (ketQua == 0) {
      // Có thiết bị trả lời ở địa chỉ này
      Serial.printf("Tim thay thiet bi o dia chi 0x%02X\n", diaChi);
      soThietBi++;
    } else if (ketQua >= 4) {
      // 4: lỗi khác trên bus, 5: quá thời gian chờ
      Serial.printf("Loi bus o dia chi 0x%02X (ma loi %u)\n", diaChi, ketQua);
    }
  }

  if (soThietBi == 0) {
    Serial.println("Khong tim thay thiet bi I2C nao. Kiem tra day noi.");
  } else {
    Serial.printf("Tong cong: %d thiet bi.\n", soThietBi);
  }
  Serial.println("-----------------------------");
  delay(5000);
}

Mở Serial Monitor ở tốc độ 115200 baud. Cứ 5 giây chương trình quét lại một lần và in địa chỉ của từng thiết bị tìm thấy:

Hình minh họa Serial Monitor khi ESP32 quét bus I2C và tìm thấy hai thiết bị ở địa chỉ 0x3C và 0x48
Kết quả mong đợi khi bus có màn hình OLED 0.96 inch và module ADS1115 (hình minh họa theo định dạng in của chương trình).
Địa chỉ I2C thường gặp của một số module
ModuleĐịa chỉ mặc địnhĐịa chỉ khác có thể gặp
Màn hình OLED 0.96 inch SSD13060x3C0x3D
Màn hình OLED 1.3 inch SH11060x3C0x3D
LCD 1602 hoặc 2004 có mạch I2C PCF85740x270x20–0x26 theo ba cầu hàn A0, A1, A2
LCD có mạch I2C dùng chip PCF8574A0x3F0x38–0x3E theo ba cầu hàn A0, A1, A2
Module ADC 16 bit ADS11150x480x49, 0x4A, 0x4B theo cách nối chân ADDR
Đồng hồ thời gian thực DS32310x68Thường kèm bộ nhớ EEPROM ở 0x57
Cảm biến BME280, BMP2800x760x77
Cảm biến gia tốc MPU60500x680x69

5. Giải thích các lệnh chính

  • Wire.begin(SDA_PIN, SCL_PIN): khởi động bus I2C trên hai chân đã chọn, tốc độ mặc định 100 kHz.
  • Wire.beginTransmission(diaChi) và Wire.endTransmission(): gọi thử một địa chỉ mà không gửi dữ liệu. Thiết bị có địa chỉ đó sẽ báo nhận.
  • Giá trị trả về của endTransmission(): 0 là có thiết bị trả lời; 2 là không có thiết bị nào ở địa chỉ đó; 4 là lỗi khác trên bus; 5 là quá thời gian chờ.
  • Vòng lặp chạy từ 0x08 đến 0x77 vì các địa chỉ ngoài khoảng này được dành riêng cho mục đích khác.

6. Đổi chân SDA và SCL

Muốn dùng hai chân khác, chỉ cần đổi hai số trong Wire.begin(). Ví dụ dùng GPIO32 làm SDA và GPIO33 làm SCL:

Wire.begin(32, 33);   // SDA = GPIO32, SCL = GPIO33
  • Không dùng GPIO34–39 cho I2C vì các chân này chỉ là ngõ vào.
  • Tránh GPIO0, 2, 12 và 15 (tham gia quá trình khởi động) và GPIO6–11 (nối với bộ nhớ flash).
  • Một số mạch gắn sẵn màn hình dùng chân I2C riêng. Ví dụ mô đun ESP32 tích hợp màn hình OLED 0.96 inch và pin 18650 dùng SDA = GPIO5, SCL = GPIO4, tức là Wire.begin(5, 4). Quét bus với đúng hai chân này sẽ thấy màn hình ở 0x3C.

7. Nối nhiều thiết bị trên một bus

Các thiết bị I2C nối song song: tất cả chân SDA nối chung với nhau, tất cả chân SCL nối chung với nhau, cùng với nguồn và GND. Điều kiện duy nhất là mỗi thiết bị phải có địa chỉ khác nhau.

Nối màn hình OLED 0.96 inch và module ADS1115 chung một bus I2C
ESP32 DevKit V1OLED 0.96 inchADS1115
3V3VCCVDD
GNDGNDGND
D22 / GPIO22SCLSCL
D21 / GPIO21SDASDA
Sơ đồ nối ESP32 DevKit V1 với màn hình OLED 0.96 inch và module ADS1115 chung bốn đường 3V3, GND, SCL, SDA
Màn hình OLED 0.96 inch (0x3C) và module ADS1115 (0x48) dùng chung bus I2C của ESP32 (hình vẽ minh họa).

Trên breadboard, cách gọn nhất là đưa bốn đường 3V3, GND, SCL, SDA từ ESP32 ra bốn hàng lỗ rồi cắm các module vào đó. Chương trình dưới đây đọc điện áp ở kênh A0 của ADS1115 và hiển thị lên màn hình OLED. Cần cài ba thư viện Adafruit SSD1306, Adafruit GFX Library và Adafruit ADS1X15 trong Tools > Manage Libraries (bài dùng các bản 2.5.17, 1.12.6 và 2.5.0).

/*
  PYWORLD.VN - Hai thiết bị trên một bus I2C: OLED 0.96 inch (0x3C) và ADS1115 (0x48)
  Cả hai module: nguồn 3V3, GND chung, SDA = GPIO21, SCL = GPIO22
  Thư viện: Adafruit SSD1306, Adafruit GFX, Adafruit ADS1X15
*/

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_ADS1X15.h>

#define SDA_PIN    21
#define SCL_PIN    22
#define OLED_ADDR  0x3C
#define ADS_ADDR   0x48

Adafruit_SSD1306 oled(128, 64, &Wire, -1);
Adafruit_ADS1115 ads;

void setup() {
  Serial.begin(115200);
  delay(500);

  Wire.begin(SDA_PIN, SCL_PIN);

  // Mỗi thiết bị được gọi bằng địa chỉ riêng của nó
  if (!oled.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    Serial.println("Khong tim thay OLED o dia chi 0x3C!");
    while (true) delay(1000);
  }
  if (!ads.begin(ADS_ADDR)) {
    Serial.println("Khong tim thay ADS1115 o dia chi 0x48!");
    while (true) delay(1000);
  }

  ads.setGain(GAIN_ONE);   // Thang đo ±4,096 V
  oled.setTextColor(SSD1306_WHITE);
}

void loop() {
  int16_t raw = ads.readADC_SingleEnded(0);
  float volt = ads.computeVolts(raw);

  oled.clearDisplay();
  oled.setTextSize(1);
  oled.setCursor(0, 4);
  oled.print("ADS1115 - kenh A0");
  oled.setTextSize(3);
  oled.setCursor(0, 26);
  oled.print(volt, 3);
  oled.setTextSize(2);
  oled.setCursor(104, 34);
  oled.print("V");
  oled.display();

  Serial.printf("A0 = %.4f V\n", volt);
  delay(500);
}

Hai thư viện dùng chung đối tượng Wire. Mỗi lần giao tiếp, thư viện gửi địa chỉ của thiết bị mình cần (0x3C hoặc 0x48), nên hai module không lẫn dữ liệu của nhau. Cách nối và chọn thang đo của ADS1115 có trong bài đo điện áp 16 bit bằng ADS1115 với ESP32.

8. Khi hai thiết bị trùng địa chỉ

Ba cách xử lý khi hai thiết bị I2C trùng địa chỉ: đổi địa chỉ trên module, dùng bus I2C thứ hai, dùng bộ chia kênh TCA9548A
Ba cách xử lý khi hai thiết bị I2C có cùng địa chỉ (hình vẽ minh họa).

Nếu module không đổi được địa chỉ, cách đơn giản nhất với ESP32 là dùng bus I2C thứ hai. Chương trình sau khởi động hai bus trên hai cặp chân khác nhau và quét lần lượt từng bus:

/*
  PYWORLD.VN - ESP32 dùng hai bus I2C độc lập và quét từng bus
  Bus 1 (Wire):  SDA = GPIO21, SCL = GPIO22
  Bus 2 (Wire1): SDA = GPIO32, SCL = GPIO33
  Không cần cài thêm thư viện
*/

#include <Wire.h>

#define SDA1_PIN 21
#define SCL1_PIN 22
#define SDA2_PIN 32
#define SCL2_PIN 33

void quetBus(TwoWire &bus, const char *ten) {
  int soThietBi = 0;

  Serial.printf("%s:\n", ten);
  for (uint8_t diaChi = 0x08; diaChi <= 0x77; diaChi++) {
    bus.beginTransmission(diaChi);
    if (bus.endTransmission() == 0) {
      Serial.printf("  Tim thay thiet bi o dia chi 0x%02X\n", diaChi);
      soThietBi++;
    }
  }
  if (soThietBi == 0) {
    Serial.println("  Khong co thiet bi nao.");
  }
}

void setup() {
  Serial.begin(115200);
  delay(500);

  Wire.begin(SDA1_PIN, SCL1_PIN);
  Wire1.begin(SDA2_PIN, SCL2_PIN);
}

void loop() {
  quetBus(Wire, "Bus 1 (GPIO21, GPIO22)");
  quetBus(Wire1, "Bus 2 (GPIO32, GPIO33)");
  Serial.println("-----------------------------");
  delay(5000);
}

Để một thư viện làm việc trên bus thứ hai, truyền &Wire1 cho thư viện đó. Ví dụ với hai thư viện đã dùng ở mục 7:

Adafruit_SSD1306 oled2(128, 64, &Wire1, -1);   // Màn hình thứ hai nằm trên bus 2
ads.begin(0x48, &Wire1);                       // ADS1115 nằm trên bus 2

9. Tốc độ bus, chiều dài dây và mức điện áp

  • Tăng tốc độ: gọi Wire.setClock(400000) sau Wire.begin() để chạy 400 kHz khi mọi thiết bị trên bus đều hỗ trợ. Thư viện Adafruit SSD1306 đã tự chuyển sang 400 kHz mỗi khi gửi dữ liệu ra màn hình.
  • Dây dài: dây càng dài thì xung càng méo. Nếu buộc phải nối xa, giữ tốc độ 100 kHz, dùng dây ngắn nhất có thể và đi dây GND sát dây tín hiệu.
  • Module cấp 5 V: mạch I2C của LCD thường kéo SDA và SCL lên đúng điện áp nguồn của nó. Nếu cấp 5 V cho mạch này rồi nối thẳng vào ESP32, GPIO sẽ phải chịu 5 V. Hãy dùng mạch chuyển mức logic như trong bài dùng màn hình LCD 2004 I2C với ESP32.

10. Xử lý lỗi thường gặp

  • Không tìm thấy thiết bị nào: kiểm tra theo thứ tự: module đã có nguồn và GND chưa, SDA và SCL có bị đổi chỗ không, hàng chân của module đã hàn chưa, hai số trong Wire.begin() có đúng với chân đang nối không.
  • Chương trình in "Loi bus" ở hàng loạt địa chỉ hoặc quét rất chậm: dây SDA hoặc SCL đang bị chạm GND, chạm nhau, hoặc một module hỏng đang giữ bus ở mức thấp. Rút từng module ra để tìm module gây lỗi.
  • Lúc thấy lúc không: dây cắm lỏng hoặc quá dài. Cắm lại, rút ngắn dây và giữ tốc độ 100 kHz.
  • Quét thấy thiết bị nhưng thư viện vẫn báo không tìm thấy: địa chỉ trong chương trình khác địa chỉ quét được. Sửa lại địa chỉ, ví dụ LCD dùng chip PCF8574A có địa chỉ 0x3F thay vì 0x27.
  • Thêm module thứ hai thì cả hai cùng lỗi: hai module trùng địa chỉ (mục 8), hoặc module mới kéo sụt nguồn 3V3.
  • Không nạp được chương trình: dùng cáp USB có truyền dữ liệu, chọn đúng cổng COM và giữ nút BOOT khi IDE dừng ở dòng Connecting...

Xem thêm: dùng màn hình OLED 1.3 inch SH1106 với ESP32, dùng màn hình LCD 2004 I2C với ESP32, đo điện áp 16 bit bằng ADS1115 với ESP32 và dùng màn hình OLED SSD1306 với ESP8266.


Tin tức liên quan

Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE
Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE

7 Lượt xem

Hướng dẫn nối load cell với module HX711 và ESP32 DevKit V1, gá load cell đúng cách, hiệu chuẩn bằng vật đã biết khối lượng và viết chương trình cân trên Arduino IDE.

Hướng dẫn làm đồng hồ Internet với ESP32, NTP và màn hình OLED 0.96 inch
Hướng dẫn làm đồng hồ Internet với ESP32, NTP và màn hình OLED 0.96 inch

5 Lượt xem

Hướng dẫn ESP32 lấy giờ Việt Nam từ Internet bằng NTP trên Arduino IDE, in ra Serial Monitor và làm đồng hồ hiển thị thứ, ngày, giờ trên màn hình OLED 0.96 inch SSD1306.

Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu và điều khiển relay bằng PubSubClient
Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu và điều khiển relay bằng PubSubClient

5 Lượt xem

Hướng dẫn dùng MQTT với ESP32 trên Arduino IDE: broker, topic, publish, subscribe, cài Mosquitto và PubSubClient, gửi dữ liệu và điều khiển module relay qua mạng nội bộ.

Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32
Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32

4 Lượt xem

Hướng dẫn dùng Bluetooth Classic của ESP32 trên Arduino IDE: ghép đôi với điện thoại Android, gửi nhận chữ bằng BluetoothSerial và điều khiển module relay bằng lệnh ON, OFF.

Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE
Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE

4 Lượt xem

Hướng dẫn bật OTA trên ESP32 bằng thư viện ArduinoOTA có sẵn, nạp chương trình qua Wi-Fi từ Arduino IDE 2.x, đặt mật khẩu và các quy tắc để không mất khả năng cập nhật từ xa.

Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano
Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano

5 Lượt xem

Hướng dẫn nối màn hình OLED 0.96 inch SSD1306 (I2C) với Arduino Nano, cài thư viện Adafruit, hiện chữ nhiều cỡ, vẽ thanh mức theo điện áp và lưu ý về bộ nhớ RAM.

Hướng dẫn đọc ADC trên ESP32 và đo điện áp pin 18650 bằng cầu phân áp
Hướng dẫn đọc ADC trên ESP32 và đo điện áp pin 18650 bằng cầu phân áp

5 Lượt xem

Hướng dẫn đọc điện áp analog bằng ADC của ESP32 trên Arduino IDE, dùng analogReadMilliVolts, làm cầu phân áp đo pin Li-ion 18650 và ước lượng phần trăm pin còn lại.

Hướng dẫn dùng Deep Sleep trên ESP32: tiết kiệm pin, đánh thức bằng timer và nút nhấn
Hướng dẫn dùng Deep Sleep trên ESP32: tiết kiệm pin, đánh thức bằng timer và nút nhấn

5 Lượt xem

Hướng dẫn đưa ESP32 vào Deep Sleep trên Arduino IDE để tiết kiệm pin: các chế độ nguồn, đánh thức bằng timer và nút nhấn, giữ biến trong bộ nhớ RTC và ước tính thời lượng pin.


Bình luận
Đã thêm vào giỏ hàng