Cảm biến nhịp tim MAX30102 với ESP32: tín hiệu PPG, đếm nhịp không cần thư viện và giới hạn của SpO2

Nguyên lý PPG, nối MAX30102 với ESP32, cấu hình thanh ghi, lọc tín hiệu và đếm nhịp tim không cần thư viện, tỉ số R của SpO2 và vì sao kết quả tự làm chỉ để học.

MAX30102 là cảm biến quang học nhỏ có hai đèn (đỏ và hồng ngoại) cùng một photodiode, dùng trong máy đo nhịp tim kẹp ngón và đồng hồ thông minh. Đặt ngón tay lên cảm biến, nó đo lượng ánh sáng dội lại; lượng máu trong ngón tay thay đổi theo mỗi nhịp tim nên ánh sáng cũng thay đổi theo. Bài này giải thích tín hiệu PPG, cách nối MAX30102 với ESP32, cách cấu hình các thanh ghi, cách lọc tín hiệu để đếm nhịp tim mà không cần thư viện, và vì sao con số SpO2 từ mạch tự làm chỉ để học.

Lưu ý: mạch trong bài là đồ học tập về xử lý tín hiệu. Kết quả không dùng để chẩn đoán, theo dõi sức khỏe hay thay cho máy đo y tế.

1. Tín hiệu PPG: đo nhịp tim bằng ánh sáng

Hình cắt ngang đầu ngón tay đặt trên cảm biến MAX30102: LED đỏ và LED hồng ngoại chiếu ánh sáng vào ngón tay, ánh sáng đi qua động mạch, tĩnh mạch rồi dội về photodiode; bên phải là biểu đồ ánh sáng bị hấp thụ gồm các lớp mô, máu tĩnh mạch, máu động mạch không đổi và phần máu động mạch thay đổi theo nhịp tim
Đèn chiếu vào ngón tay, photodiode đo ánh sáng dội lại; phần thay đổi theo nhịp tim rất nhỏ so với phần không đổi (hình minh họa).

PPG (photoplethysmogram) là tín hiệu ánh sáng thay đổi theo lượng máu trong mô. Ánh sáng chiếu vào ngón tay bị da, mô, xương và máu hấp thụ một phần. Hầu hết những thứ này không đổi theo thời gian, tạo nên phần DC (một chiều). Riêng máu động mạch tăng lên mỗi khi tim co bóp rồi giảm dần, tạo nên phần AC (xoay chiều) nhỏ, chỉ khoảng 0,5–2% tổng ánh sáng nhận được.

Vì phần AC quá nhỏ, cảm biến cần bộ chuyển đổi tương tự – số (ADC) có độ phân giải cao. MAX30102 có sẵn ADC 18 bit, mạch khử ánh sáng môi trường và bộ đệm FIFO 32 mẫu, giao tiếp I2C ở địa chỉ 0x57.

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

3. Nối MAX30102 với ESP32

Sơ đồ nối module MAX30102 với ESP32 DevKit V1: VIN vào 3V3, SCL vào D22, SDA vào D21, GND vào GND, chân INT để trống
VIN vào 3V3, SDA vào D21, SCL vào D22; chân INT không dùng trong bài (hình minh họa).
MAX30102ESP32 DevKit V1Ghi chú
VIN3V3Module có mạch ổn áp 1,8 V cho lõi chip
SCLD22I2C, chạy 400 kHz
SDAD21
INTđể trốngBáo có dữ liệu mới; bài đọc FIFO theo vòng lặp nên không cần
GNDGND

Lỗi hay gặp với module màu tím: trên một số bo, điện trở kéo lên của SDA, SCL nối về đường 1,8 V. Mức 1,8 V thấp hơn mức ESP32 coi chắc chắn là mức cao, nên lúc đọc được lúc không. Để module yên (chưa chạy chương trình), đo SDA so với GND: thấy khoảng 1,8 V thì chuyển cầu hàn trên module sang 3,3 V (nếu bo có), hoặc thêm hai điện trở 4,7 kΩ từ SDA và SCL lên 3V3.

4. Cấu hình MAX30102 qua thanh ghi

MAX30102 được điều khiển bằng cách ghi vào thanh ghi qua I2C, giống như nhiều cảm biến khác (xem bài I2C là gì: đọc ghi thanh ghi trên ESP32). Chương trình ghi các giá trị sau:

Thanh ghiGiá trịÝ nghĩa
0xFF (mã linh kiện)đọc ra 0x15Kiểm tra đúng là MAX30102
0x09 (chế độ)0x40, rồi 0x03Đặt lại toàn bộ; sau đó chọn chế độ SpO2 (đo cả đèn đỏ và hồng ngoại)
0x04, 0x05, 0x060Đưa con trỏ ghi, bộ đếm tràn, con trỏ đọc của FIFO về 0
0x08 (cấu hình FIFO)0x50Lấy trung bình 4 mẫu; FIFO đầy thì ghi đè mẫu cũ
0x0A (cấu hình đo)0x2FDải ADC 4096 nA, 400 mẫu/giây, độ rộng xung đèn 411 µs (18 bit)
0x0C, 0x0D (dòng đèn)0x24Đèn đỏ và hồng ngoại 7,2 mA (mỗi bước 0,2 mA)

400 mẫu/giây lấy trung bình 4 mẫu thì FIFO nhận 100 mẫu/giây, đủ cho tín hiệu nhịp tim (thường dưới 3 Hz) mà vẫn giảm được nhiễu. Mỗi mẫu trong FIFO dài 6 byte: 3 byte đèn đỏ rồi 3 byte hồng ngoại, mỗi số lấy 18 bit thấp. Số mẫu chưa đọc bằng (con trỏ ghi − con trỏ đọc) lấy 5 bit thấp, vì FIFO chỉ có 32 ô.

5. Từ số đo thô tới nhịp tim

Hai đồ thị trong 5 giây: trên là số đo IR thô dao động quanh 120000 với các rãnh nhỏ theo nhịp tim; dưới là tín hiệu sau khi bỏ phần DC và lọc, có đỉnh nhọn mỗi nhịp, đường ngưỡng trên giảm dần, chấm đánh dấu nhịp được đếm và khoảng cách giữa các nhịp khoảng 0,8 giây
Số đo IR gần như phẳng; phần thay đổi theo nhịp tim chỉ hiện rõ sau khi lọc (tín hiệu mô phỏng).

Số đo hồng ngoại khi có ngón tay vào khoảng hàng trăm nghìn, còn phần thay đổi theo nhịp tim chỉ vài trăm. Chương trình xử lý từng mẫu qua các bước:

  1. Phát hiện ngón tay: IR dưới 50.000 coi như không có ngón tay. Vừa đặt ngón tay vào thì mọi bộ lọc bắt đầu lại.
  2. Bỏ phần DC: trung bình trượt chậm dcIr += (ir − dcIr) / 64 theo dõi phần không đổi; lấy dcIr − ir được phần AC. Dấu được đảo vì khi tim co bóp, máu nhiều hơn, ánh sáng về ít hơn: đảo dấu để đỉnh tín hiệu trùng lúc tim co bóp.
  3. Lọc thông thấp: locIr += (ac − locIr) × 0,25 làm mượt, bớt nhiễu nhanh.
  4. Đếm nhịp có hai ngưỡng: biên độ đỉnh dinh giảm dần theo thời gian và được kéo lên mỗi khi tín hiệu cao hơn. Một nhịp được đếm khi tín hiệu vượt 0,6 × dinh, và phải xuống dưới 0 rồi mới đếm nhịp tiếp theo. Mỗi nhịp PPG có một khấc phụ nhỏ sau đỉnh chính; ngưỡng 0,6 cao hơn khấc này nên không bị đếm đôi.
  5. Loại khoảng vô lý: hai nhịp cách nhau dưới 0,3 giây (trên 200 nhịp/phút) coi là nhiễu; trên 2 giây (dưới 30 nhịp/phút) thì không dùng khoảng đó.
  6. Tính nhịp tim: 60 chia cho khoảng cách trung bình của 4 nhịp gần nhất (tính bằng giây).

Chương trình chờ 2 giây cho bộ lọc ổn định và cần đủ 5 nhịp, nên kết quả đầu tiên có sau khoảng 6–7 giây từ lúc đặt ngón tay.

6. Tỉ số R và SpO2: nguyên lý và giới hạn

Công thức R bằng AC đỏ chia DC đỏ, chia cho AC hồng ngoại chia DC hồng ngoại; đồ thị đường cong SpO2 ước tính giảm từ 100% khi R bằng 0,4 xuống khoảng 80% khi R bằng 1, có chấm tại R 0,53 ứng với khoảng 98%
SpO2 được suy ra từ tỉ số R qua một đường cong hiệu chuẩn (số liệu mô phỏng).

SpO2 là tỉ lệ hemoglobin trong máu động mạch đang mang oxy. Hemoglobin mang oxy hấp thụ ít ánh sáng đỏ hơn hemoglobin không mang oxy, còn với ánh sáng hồng ngoại thì ngược lại. Vì vậy so độ dao động của hai màu đèn cho biết tỉ lệ này:

R = (AC đỏ / DC đỏ) ÷ (AC hồng ngoại / DC hồng ngoại)

Chia cho DC để loại ảnh hưởng của độ dày ngón tay và độ sáng đèn. Chương trình tính AC bằng độ lệch chuẩn của tín hiệu đã lọc trong mỗi nhịp (có trừ giá trị trung bình để phần trôi chậm còn sót không làm lệch R), lấy trung bình 4 nhịp, rồi đổi R ra SpO2 bằng công thức kinh nghiệm dùng trong mã mẫu của hãng: SpO2 = −45,060 R² + 30,354 R + 94,845.

Giới hạn cần biết:

  • Công thức này chỉ đúng với cảm biến và vỏ đã được hiệu chuẩn trên người thật, so với máy đo chuẩn. Module bán rời chưa qua bước đó.
  • R lệch 0,05 đã làm kết quả đổi 1–3%. Cử động, ấn ngón tay mạnh, tay lạnh, ánh sáng lọt vào đều làm R lệch nhiều hơn thế.
  • Máy đo kẹp ngón dùng trong y tế có vỏ che sáng, lò xo giữ lực ép ổn định, thuật toán chống nhiễu và được kiểm định.
Máy đo SpO2 kẹp ngón tay màu trắng, màn hình hiện SpO2 96 phần trăm, nhịp 78 và dạng sóng PPG màu xanh
Máy đo SpO2 kẹp ngón hiện cả dạng sóng PPG. Ảnh: TomTrottier.com, CC BY-SA 4.0, Wikimedia Commons; đã cắt, thu nhỏ.

7. Chương trình

Chương trình chỉ dùng thư viện Wire có sẵn trong gói esp32. Đặt VE_DO_THI = true để in tín hiệu đã lọc và ngưỡng cho Serial Plotter của Arduino IDE, rất tiện để thấy ngón tay đặt đã ổn chưa.

// PYWORLD.VN - Cảm biến MAX30102 với ESP32: đọc tín hiệu PPG, đếm nhịp tim, tính tỉ số R của hai đèn đỏ và hồng ngoại, không cần thư viện
// Nối: VIN 3V3, GND, SDA D21, SCL D22 (địa chỉ I2C 0x57). Đặt đầu ngón tay nhẹ và giữ yên trên mặt cảm biến
// Chỉ để học nguyên lý đo: không dùng kết quả để chẩn đoán hay theo dõi sức khỏe

#include <Wire.h>

const uint8_t DIA_CHI = 0x57;
const bool VE_DO_THI = false;              // true: in tín hiệu cho Serial Plotter; false: in kết quả mỗi giây
const uint32_t NGUONG_NGON_TAY = 50000;    // IR thấp hơn mức này: không có ngón tay trên cảm biến
const int TAN_SO_MAU = 100;                // 400 mẫu/giây, lấy trung bình 4 mẫu → 100 mẫu/giây

const uint8_t TG_FIFO_GHI = 0x04, TG_FIFO_TRAN = 0x05, TG_FIFO_DOC = 0x06, TG_FIFO_DATA = 0x07;
const uint8_t TG_FIFO_CAU_HINH = 0x08, TG_CHE_DO = 0x09, TG_SPO2_CAU_HINH = 0x0A;
const uint8_t TG_LED_DO = 0x0C, TG_LED_IR = 0x0D, TG_PART_ID = 0xFF;

bool ghi(uint8_t tg, uint8_t giaTri) {
  Wire.beginTransmission(DIA_CHI);
  Wire.write(tg);
  Wire.write(giaTri);
  return Wire.endTransmission() == 0;
}

int doc(uint8_t tg) {
  Wire.beginTransmission(DIA_CHI);
  Wire.write(tg);
  if (Wire.endTransmission(false) != 0 || Wire.requestFrom(DIA_CHI, (uint8_t)1) != 1) return -1;
  return Wire.read();
}

bool khoiDong() {
  if (doc(TG_PART_ID) != 0x15) return false;           // MAX30102 có mã linh kiện 0x15
  ghi(TG_CHE_DO, 0x40);                                // đặt lại toàn bộ thanh ghi
  delay(10);
  ghi(TG_FIFO_GHI, 0);
  ghi(TG_FIFO_TRAN, 0);
  ghi(TG_FIFO_DOC, 0);
  ghi(TG_FIFO_CAU_HINH, 0x50);                         // trung bình 4 mẫu, FIFO đầy thì ghi đè mẫu cũ
  ghi(TG_SPO2_CAU_HINH, 0x2F);                         // dải 4096 nA, 400 mẫu/giây, xung 411 µs (18 bit)
  ghi(TG_LED_DO, 0x24);                                // dòng đèn đỏ 7,2 mA (mỗi bước 0,2 mA)
  ghi(TG_LED_IR, 0x24);                                // dòng đèn hồng ngoại 7,2 mA
  return ghi(TG_CHE_DO, 0x03);                         // chế độ SpO2: đo cả đèn đỏ và hồng ngoại
}

// Mỗi mẫu trong FIFO 6 byte: 3 byte đèn đỏ rồi 3 byte hồng ngoại, mỗi số 18 bit.
bool docMau(uint32_t &doDo, uint32_t &hongNgoai) {
  Wire.beginTransmission(DIA_CHI);
  Wire.write(TG_FIFO_DATA);
  if (Wire.endTransmission(false) != 0 || Wire.requestFrom(DIA_CHI, (uint8_t)6) != 6) return false;
  uint8_t b[6];
  for (int i = 0; i < 6; i++) b[i] = Wire.read();
  doDo = (((uint32_t)b[0] << 16) | (b[1] << 8) | b[2]) & 0x3FFFF;
  hongNgoai = (((uint32_t)b[3] << 16) | (b[4] << 8) | b[5]) & 0x3FFFF;
  return true;
}

// ---- Xử lý tín hiệu ----
float dcIr, dcDo, locIr, locDo, dinh;      // thành phần một chiều, tín hiệu đã lọc, biên độ đỉnh gần đây
bool coNgon = false, duoiNguong = true;
uint32_t soMau = 0, mauBatDau = 0, mauNhipTruoc = 0;
float khoangNhip[4], tiSoR[4];             // 4 nhịp gần nhất
int soNhip = 0, viTri = 0;
double tongIr, tongIr2, tongDo, tongDo2;   // tổng và tổng bình phương phần xoay chiều trong một nhịp
uint32_t soMauTrongNhip;

// Độ lệch chuẩn trong một nhịp: trừ giá trị trung bình để phần trôi chậm còn sót không làm lệch tỉ số R.
float doLech(double tong, double tong2, uint32_t n) {
  double tb = tong / n;
  return sqrt(tong2 / n - tb * tb);
}

void xuLyMau(uint32_t doDo, uint32_t hongNgoai) {
  soMau++;
  if (hongNgoai < NGUONG_NGON_TAY) {
    coNgon = false;
    return;
  }
  if (!coNgon) {                           // vừa đặt ngón tay: bắt đầu lại từ đầu
    coNgon = true;
    dcIr = hongNgoai;
    dcDo = doDo;
    locIr = locDo = dinh = 0;
    duoiNguong = true;
    soNhip = viTri = 0;
    mauBatDau = soMau;
    mauNhipTruoc = 0;
    tongIr = tongIr2 = tongDo = tongDo2 = 0;
    soMauTrongNhip = 0;
  }
  dcIr += (hongNgoai - dcIr) / 64.0f;      // trung bình trượt chậm: phần một chiều (mô, máu tĩnh mạch)
  dcDo += (doDo - dcDo) / 64.0f;
  // Tim co bóp, máu động mạch nhiều hơn nên ánh sáng về ít hơn: đảo dấu để đỉnh tín hiệu là lúc tim co bóp
  locIr += ((dcIr - hongNgoai) - locIr) * 0.25f;     // lọc thông thấp, bớt nhiễu nhanh
  locDo += ((dcDo - doDo) - locDo) * 0.25f;
  dinh *= 0.995f;                          // biên độ đỉnh giảm dần (còn một nửa sau khoảng 1,4 giây)
  if (locIr > dinh) dinh = locIr;
  tongIr += locIr;
  tongIr2 += (double)locIr * locIr;
  tongDo += locDo;
  tongDo2 += (double)locDo * locDo;
  soMauTrongNhip++;
  if (soMau - mauBatDau < 2 * TAN_SO_MAU) return;    // chờ 2 giây cho bộ lọc ổn định

  float nguong = 0.6f * dinh;              // ngưỡng trên cao hơn khấc phụ sau mỗi nhịp
  if (duoiNguong && locIr > nguong) {      // tín hiệu vượt ngưỡng trên: một nhịp mới
    uint32_t khoang = soMau - mauNhipTruoc;
    if (mauNhipTruoc != 0 && khoang < TAN_SO_MAU * 3 / 10) return;   // dưới 0,3 giây: nhiễu, bỏ qua
    duoiNguong = false;
    if (mauNhipTruoc != 0 && khoang <= 2 * TAN_SO_MAU) {            // 0,3–2 giây: 30–200 nhịp/phút
      float acDo = doLech(tongDo, tongDo2, soMauTrongNhip), acIr = doLech(tongIr, tongIr2, soMauTrongNhip);
      khoangNhip[viTri] = khoang;
      tiSoR[viTri] = (acDo / dcDo) / (acIr / dcIr);
      viTri = (viTri + 1) % 4;
      if (soNhip < 4) soNhip++;
    }
    mauNhipTruoc = soMau;
    tongIr = tongIr2 = tongDo = tongDo2 = 0;
    soMauTrongNhip = 0;
  } else if (!duoiNguong && locIr < 0) {
    duoiNguong = true;                     // phải xuống dưới 0 mới đếm nhịp tiếp theo
  }
  if (VE_DO_THI) Serial.printf("ir:%.0f,nguong:%.0f\n", locIr, nguong);
}

void inKetQua(uint32_t hongNgoai) {
  if (!coNgon) {
    Serial.printf("Dat ngon tay len cam bien (IR = %lu)\n", (unsigned long)hongNgoai);
  } else if (soNhip < 4) {
    Serial.printf("Dang do, giu yen ngon tay... (IR = %lu)\n", (unsigned long)hongNgoai);
  } else {
    float tongKhoang = 0, tongR = 0;
    for (int i = 0; i < 4; i++) {
      tongKhoang += khoangNhip[i];
      tongR += tiSoR[i];
    }
    float nhipPhut = 60.0f * TAN_SO_MAU / (tongKhoang / 4);
    float r = tongR / 4;
    float spo2 = -45.060f * r * r + 30.354f * r + 94.845f;       // công thức kinh nghiệm, cần hiệu chuẩn
    Serial.printf("IR %lu | nhip tim %.0f lan/phut | R %.2f | SpO2 uoc tinh %.0f%% (chi de hoc)\n",
                  (unsigned long)hongNgoai, nhipPhut, r, spo2 > 100 ? 100 : spo2);
  }
}

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);
  Wire.setClock(400000);
  while (!khoiDong()) {
    Serial.println("Khong thay MAX30102 o dia chi 0x57: kiem tra SDA, SCL, nguon 3V3");
    delay(2000);
  }
  if (!VE_DO_THI) Serial.println("MAX30102 san sang. Chi de hoc nguyen ly, khong dung cho y te");
}

void loop() {
  int ghiP = doc(TG_FIFO_GHI), docP = doc(TG_FIFO_DOC);
  if (ghiP < 0 || docP < 0) return;
  int soMoi = (ghiP - docP) & 0x1F;        // FIFO 32 mẫu: số mẫu chưa đọc
  uint32_t doDo = 0, hongNgoai = 0;
  for (int i = 0; i < soMoi; i++) {
    if (!docMau(doDo, hongNgoai)) return;
    xuLyMau(doDo, hongNgoai);
    if (!VE_DO_THI && soMau % TAN_SO_MAU == 0) inKetQua(hongNgoai);
  }
}
  • khoiDong() kiểm tra mã linh kiện 0x15 trước khi cấu hình; sai mã thì báo lỗi và thử lại mỗi 2 giây.
  • loop() đọc hai con trỏ FIFO để biết có bao nhiêu mẫu mới, đọc hết rồi đưa từng mẫu vào xuLyMau(). Cách này không bỏ sót mẫu dù vòng lặp chạy chậm, miễn là chưa quá 32 mẫu (0,32 giây).
  • doLech() tính độ lệch chuẩn từ tổng và tổng bình phương, không cần lưu cả nhịp vào mảng.

8. Kết quả mong đợi

Serial Monitor: dòng đặt ngón tay lên cảm biến với IR khoảng 1186, các dòng đang đo với IR khoảng 120000, rồi các dòng nhịp tim 70 đến 74 lần mỗi phút, R khoảng 0,55, SpO2 ước tính 97 đến 98 phần trăm; khung dưới báo không thấy MAX30102 ở địa chỉ 0x57
Mỗi giây một dòng; khung dưới là khi không đọc được cảm biến (số liệu mô phỏng).

Cách đặt ngón tay quyết định chất lượng tín hiệu:

  • Đặt đầu ngón tay (phần thịt, không phải móng) phủ kín mặt cảm biến, ấn nhẹ. Ấn mạnh làm máu bị ép ra khỏi mao mạch, tín hiệu AC nhỏ hẳn đi.
  • Giữ yên tay và ngồi yên. Cử động tạo dao động lớn hơn nhiều lần tín hiệu nhịp tim.
  • Tránh đèn chiếu thẳng vào cảm biến; tay lạnh thì xoa cho ấm trước khi đo.
  • Dùng dây chun hoặc băng dán giữ ngón tay với lực đều sẽ ổn định hơn tự giữ bằng tay.

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

Hiện tượngNguyên nhân thường gặpCách xử lý
"Khong thay MAX30102 o dia chi 0x57"Đảo SDA/SCL; kéo lên 1,8 V; module khác loại (MAX30100 có mã khác)Quét I2C; kiểm tra mức SDA; đọc chữ trên chip
Luôn "Dat ngon tay len cam bien" dù đã đặtIR thấp hơn 50.000: đèn yếu, ngón tay chưa phủ kínĐặt lại ngón tay; tăng dòng đèn (0x0C, 0x0D) lên 0x30–0x3F
IR gần 262.143 và không đổiADC bão hòa: đèn quá sángGiảm dòng đèn hoặc chọn dải ADC 8192 nA
Nhịp tim gấp đôi thực tếKhấc phụ bị đếm thành nhịp; nhiễu do cử độngGiữ yên; xem Serial Plotter, tăng hệ số ngưỡng từ 0,6 lên 0,7
Nhịp tim nhảy lung tung, R thay đổi nhiềuCử động, ấn quá mạnh, ánh sáng lọt vàoẤn nhẹ, che sáng, cố định ngón tay bằng dây chun
Mãi "Dang do" không ra kết quảTín hiệu quá yếu, không vượt ngưỡng đềuBật VE_DO_THI xem dạng sóng; xoa ấm tay; đổi ngón khác

10. Xem thêm


Tin tức liên quan

Module thời gian thực DS3231 với ESP32: đọc giờ, đặt giờ, báo thức hằng ngày, giữ giờ khi mất điện
Module thời gian thực DS3231 với ESP32: đọc giờ, đặt giờ, báo thức hằng ngày, giữ giờ khi mất điện

DS3231 khác DS1307 thế nào, nối với ESP32 qua I2C, thanh ghi và mã BCD, chương trình đọc giờ, nhiệt độ, đặt giờ qua Serial và báo thức hằng ngày qua chân SQW, kèm bảng lỗi.

ESP32 gửi cảnh báo Telegram, nhận lệnh bật tắt relay: tạo bot, lấy chat id, không cần thư viện bot
ESP32 gửi cảnh báo Telegram, nhận lệnh bật tắt relay: tạo bot, lấy chat id, không cần thư viện bot

Tạo bot Telegram, lấy token và chat id, cho ESP32 gửi cảnh báo cửa mở, báo có điện lại và nhận lệnh /bat, /tat relay bằng HTTPClient và ArduinoJson.

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 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ỉ.

Điều khiển relay bằng điện thoại với Blynk IoT và ESP32: template, datastream, đồng bộ trạng thái
Điều khiển relay bằng điện thoại với Blynk IoT và ESP32: template, datastream, đồng bộ trạng thái

Hướng dẫn Blynk IoT với ESP32 từ đầu: tạo template, datastream V0–V5, thiết bị và token, nối relay 4 kênh, chương trình bật tắt bằng điện thoại kèm nút nhấn tại chỗ.

Hướng dẫn dùng màn hình OLED SSD1306 với ESP8266
Hướng dẫn dùng màn hình OLED SSD1306 với ESP8266

Hướng dẫn nối OLED SSD1306 với ESP8266, quét địa chỉ I2C, cài thư viện và hiển thị thời gian bằng mã Arduino mẫu.

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

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.

ESPHome cho ESP32: đưa relay 4 kênh vào Home Assistant không cần viết code Arduino
ESPHome cho ESP32: đưa relay 4 kênh vào Home Assistant không cần viết code Arduino

Dùng ESPHome Device Builder khai báo relay 4 kênh, nút nhấn, cảm biến Wi-Fi bằng YAML; nạp lần đầu qua USB, sau đó qua Wi-Fi; so sánh với cách Arduino + MQTT.

Đồng hồ và dự báo thời tiết trên OLED: ESP32 lấy dữ liệu Open-Meteo qua HTTP API
Đồng hồ và dự báo thời tiết trên OLED: ESP32 lấy dữ liệu Open-Meteo qua HTTP API

ESP32 lấy giờ NTP và thời tiết Open-Meteo qua HTTPS mỗi 15 phút, đọc JSON bằng ArduinoJson, hiện nhiệt độ, độ ẩm, gió và dự báo 3 ngày trên OLED 0.96 inch.


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