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.
- Đị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à
Wirevà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ị
- ESP32 DevKit V1 30 chân (hoặc bản dùng chip CH340) và cáp USB có truyền dữ liệu.
- Một hoặc nhiều module I2C. Bài dùng màn hình OLED 0.96 inch SSD1306 và mạch chuyển đổi ADC 16 bit ADS1115.
- Breadboard MB-102 và dây cắm test board.
- Máy tính đã cài Arduino IDE 2.x và gói esp32 by Espressif Systems (bài dùng phiên bản 3.3.10).
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:
| Module | Địa chỉ mặc định | Địa chỉ khác có thể gặp |
|---|---|---|
| Màn hình OLED 0.96 inch SSD1306 | 0x3C | 0x3D |
| Màn hình OLED 1.3 inch SH1106 | 0x3C | 0x3D |
| LCD 1602 hoặc 2004 có mạch I2C PCF8574 | 0x27 | 0x20–0x26 theo ba cầu hàn A0, A1, A2 |
| LCD có mạch I2C dùng chip PCF8574A | 0x3F | 0x38–0x3E theo ba cầu hàn A0, A1, A2 |
| Module ADC 16 bit ADS1115 | 0x48 | 0x49, 0x4A, 0x4B theo cách nối chân ADDR |
| Đồng hồ thời gian thực DS3231 | 0x68 | Thường kèm bộ nhớ EEPROM ở 0x57 |
| Cảm biến BME280, BMP280 | 0x76 | 0x77 |
| Cảm biến gia tốc MPU6050 | 0x68 | 0x69 |
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.
| ESP32 DevKit V1 | OLED 0.96 inch | ADS1115 |
|---|---|---|
| 3V3 | VCC | VDD |
| GND | GND | GND |
| D22 / GPIO22 | SCL | SCL |
| D21 / GPIO21 | SDA | SDA |
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ỉ
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)sauWire.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.
Xem thêm