Cảm ứng điện trở XPT2046 với ESP32: hiệu chuẩn ba điểm, lọc nhiễu, vẽ nút bấm trên màn TFT

Màn TFT cảm ứng XPT2046 với ESP32: nguyên lý cảm ứng điện trở, nối dây SPI chung, vì sao chạm lệch, hiệu chuẩn ba điểm so với map(), chương trình lưu hệ số vào flash và vẽ nút bấm.

XPT2046 là chip đọc cảm ứng điện trở gắn trên hầu hết màn TFT SPI 2,4–3,2 inch giá rẻ. Lỗi hay gặp nhất với loại màn này là bấm một chỗ, máy nhận chỗ khác: nút bấm lệch vài chục pixel, trục bị ngược, chạm góc này mà nhảy sang góc kia. Bài này giải thích vì sao phải hiệu chuẩn, so sánh cách map() hai điểm với hiệu chuẩn ba điểm bằng mô phỏng, rồi đưa chương trình ESP32 hoàn chỉnh: tự hiệu chuẩn, lưu số vào flash, vẽ nút bấm có phản hồi.

1. Cảm ứng điện trở hoạt động thế nào

Hai lớp phủ điện trở của màn cảm ứng: bút ấn làm hai lớp chạm nhau tạo cầu phân áp, XPT2046 đọc bằng ADC 12 bit qua SPI, chân T_IRQ báo có chạm
Vị trí chạm chia một lớp điện trở thành hai đoạn.

Màn có hai lớp phim phủ điện trở, cách nhau bằng các hạt đệm rất nhỏ. Khi ấn, hai lớp chạm nhau. XPT2046 cấp điện áp cho một lớp rồi đọc điện áp trên lớp kia bằng ADC 12 bit, ra số từ 0 đến 4095 tỉ lệ với vị trí. Đảo vai hai lớp để đo trục còn lại, và đo thêm điện trở tiếp xúc để biết lực ấn (số Z).

Từ đó có ba đặc điểm cần nhớ khi lập trình: cần lực ấn nên dùng bút hoặc móng tay tốt hơn đầu ngón; lần đọc lúc vừa chạm hoặc vừa nhả thường lệch, nên phải lọc; và chỉ nhận một điểm, ấn hai điểm thì ra vị trí ở giữa.

2. Nối dây

Sơ đồ nối màn TFT ILI9341 có XPT2046 với ESP32 DevKit V1: CS D15, RESET D4, DC D2, SDI và T_DIN D23, SCK và T_CLK D18, SDO và T_DO D19, T_CS D21, LED 3V3
Màn và cảm ứng chung ba dây SPI, tách nhau bằng hai chân CS.

Trên module thường có hai nhóm chân: nhóm SDI, SCK, SDO, CS cho màn và nhóm T_DIN, T_CLK, T_DO, T_CS cho cảm ứng. Nối chung ba đường dữ liệu và xung nhịp để tiết kiệm chân. SDO của màn và T_DO đều phải nối D19, thiếu dây này thì cảm ứng không đọc được, dù màn vẫn hiện hình.

Cấu hình TFT_eSPI cần thêm dòng #define TOUCH_CS 21 và tốc độ SPI_TOUCH_FREQUENCY 2500000. XPT2046 chỉ chạy được khoảng 2,5 MHz, thư viện tự hạ tốc độ khi đọc cảm ứng rồi trả lại tốc độ cao cho màn. Cách tạo file cấu hình có ở bài cùng cụm về cấu hình TFT_eSPI; phần màn hình ILI9341 xem bài màn hình TFT SPI ILI9341 với ESP32.

3. Vì sao phải hiệu chuẩn, và chọn cách nào

Số thô từ XPT2046 chưa phải pixel: tấm cảm ứng là linh kiện rời dán lên màn nên hơi lệch, hơi xoay; phần viền không tới được giá trị 0 và 4095; trục thô có thể ngược hoặc đổi chỗ so với trục màn. Cách phổ biến là dùng map() theo số đo hai góc:

int x = map(rawY, 3780, 320, 0, 320);   // chỉ co giãn từng trục độc lập
int y = map(rawX, 3850, 280, 0, 240);

Cách này xử lý được co giãn và đảo chiều, nhưng không tính độ xoay. Hiệu chuẩn ba điểm tìm phép biến đổi affine, tức 6 hệ số:

x = A·rx + B·ry + C
y = D·rx + E·ry + F

Hai hệ số B và D cho phép mỗi trục màn phụ thuộc cả hai trục thô, nên xử lý được cả xoay, đảo chiều và đổi trục. Chạm 3 điểm trên màn cho 6 phương trình, vừa đủ giải 6 ẩn. Ba điểm phải không thẳng hàng, đặt xa nhau để nhiễu ít ảnh hưởng.

Hai bản đồ sai số trên màn 320 x 240: map hai điểm sai trung bình 2,7 pixel lớn nhất 7,4 pixel với các mũi tên lệch, affine ba điểm sai 0,3 pixel lớn nhất 0,8 pixel
Mô phỏng với tấm cảm ứng xoay 1,2 độ.

Theo mô phỏng dưới đây, với tấm dán xoay 1,2 độ, map() để lại sai số tới 7,4 pixel ở các góc, còn affine chỉ khoảng 0,8 pixel. Nếu nút bấm lớn thì map() vẫn dùng được; nhưng với bàn phím số trên màn nhỏ, chênh lệch này là khác biệt giữa bấm đúng và bấm nhầm phím. Hiệu chuẩn ba điểm là cách làm phổ biến trong các tài liệu ứng dụng về màn cảm ứng điện trở.

# PYWORLD.VN - So sánh hiệu chuẩn cảm ứng XPT2046: map() hai điểm và phép biến đổi affine ba điểm, trên tấm cảm ứng dán lệch
# PYWORLD.VN - Giả định: màn 320 x 240, tấm cảm ứng xoay 1,2 độ, trục X Y của tấm ngược với màn, nhiễu ADC khoảng 6 đơn vị
# PYWORLD.VN - Chạy: python mo_phong_hieu_chuan.py          (chỉ dùng thư viện chuẩn)
"""Công thức ba điểm giống hệt hàm tinhHeSo() trong file .ino."""
import math
import random

RONG, CAO = 320, 240
GOC = math.radians(1.2)
DIEM_CHUAN = [(32, 24), (288, 120), (160, 216)]


def man_sang_tho(x: float, y: float, rng=None) -> tuple[float, float]:
    """Điểm chạm (pixel) sang số ADC thô của XPT2046 theo mô hình tấm dán lệch."""
    xq = x * math.cos(GOC) - y * math.sin(GOC)
    yq = x * math.sin(GOC) + y * math.cos(GOC)
    rx = 3850 - yq * 14.9                       # trục thô X ứng với trục Y màn, chiều ngược
    ry = 3780 - xq * 10.8
    if rng:
        rx += rng.gauss(0, 6)
        ry += rng.gauss(0, 6)
    return rx, ry


def tinh_he_so(man: list, tho: list) -> tuple[float, ...]:
    """Tìm A…F sao cho x = A·rx + B·ry + C, y = D·rx + E·ry + F đi qua đúng 3 điểm."""
    (x1, y1), (x2, y2), (x3, y3) = man
    (r1, s1), (r2, s2), (r3, s3) = tho
    d = (r1 - r3) * (s2 - s3) - (r2 - r3) * (s1 - s3)
    a = ((x1 - x3) * (s2 - s3) - (x2 - x3) * (s1 - s3)) / d
    b = ((r1 - r3) * (x2 - x3) - (r2 - r3) * (x1 - x3)) / d
    c = x3 - a * r3 - b * s3
    e = ((r1 - r3) * (y2 - y3) - (r2 - r3) * (y1 - y3)) / d
    dd = ((y1 - y3) * (s2 - s3) - (y2 - y3) * (s1 - s3)) / d
    f = y3 - dd * r3 - e * s3
    return a, b, c, dd, e, f


def ba_diem(he_so, rx, ry):
    a, b, c, d, e, f = he_so
    return a * rx + b * ry + c, d * rx + e * ry + f


def hai_diem(rx, ry):
    """Cách hay gặp: map() độc lập từng trục theo hai góc đo được, bỏ qua độ xoay."""
    r0, s0 = man_sang_tho(0, 0)
    r1, s1 = man_sang_tho(RONG, CAO)
    return (ry - s0) / (s1 - s0) * RONG, (rx - r0) / (r1 - r0) * CAO


def luoi(so_x: int = 9, so_y: int = 7):
    return [(RONG * (i + 0.5) / so_x, CAO * (j + 0.5) / so_y) for j in range(so_y) for i in range(so_x)]


def do_sai(ham, rng) -> list[tuple[float, float, float, float]]:
    """Với mỗi điểm lưới, lấy trung bình 8 lần đo; trả về (x, y, sai x, sai y)."""
    ra = []
    for x, y in luoi():
        tx, ty = 0.0, 0.0
        for _ in range(8):
            mx, my = ham(*man_sang_tho(x, y, rng))
            tx += mx / 8
            ty += my / 8
        ra.append((x, y, tx - x, ty - y))
    return ra


def thong_ke(sai) -> tuple[float, float]:
    d = [math.hypot(a, b) for _, _, a, b in sai]
    return sum(d) / len(d), max(d)


def chay():
    rng = random.Random(3)
    tho = []
    for x, y in DIEM_CHUAN:
        mau = [man_sang_tho(x, y, rng) for _ in range(16)]
        tho.append((sum(m[0] for m in mau) / 16, sum(m[1] for m in mau) / 16))
    he_so = tinh_he_so(DIEM_CHUAN, tho)
    return do_sai(hai_diem, rng), do_sai(lambda r, s: ba_diem(he_so, r, s), rng), he_so


if __name__ == "__main__":
    s2, s3, hs = chay()
    print("He so A..F:", [round(v, 5) for v in hs])
    print(f"map hai diem : sai trung binh {thong_ke(s2)[0]:.1f} px, lon nhat {thong_ke(s2)[1]:.1f} px")
    print(f"affine ba diem: sai trung binh {thong_ke(s3)[0]:.1f} px, lon nhat {thong_ke(s3)[1]:.1f} px")

4. Chương trình ESP32

Chương trình dùng TFT_eSPI 2.5.43 trên ESP32, gói esp32 3.3.10. Lần đầu chạy, màn hiện lần lượt ba dấu cộng; chạm đúng tâm bằng bút cảm ứng. Số hệ số được lưu vào flash, lần sau không phải làm lại. Thêm một LED ở chân D25 qua điện trở 330 Ω để thấy nút hoạt động.

// PYWORLD.VN - Màn ILI9341 có cảm ứng XPT2046 với ESP32: hiệu chuẩn ba điểm (affine), lưu vào flash, vẽ nút bấm có phản hồi
// PYWORLD.VN - Dùng TFT_eSPI 2.5.43 với file cấu hình có TOUCH_CS 21; cảm ứng dùng chung SPI với màn, không cần thư viện cảm ứng riêng
// PYWORLD.VN - Giữ nút HIEU CHUAN để đo lại. Lần đầu chạy, chạm đúng tâm từng dấu cộng bằng bút cảm ứng

#include <Preferences.h>
#include <TFT_eSPI.h>

const int CHAN_LED = 25;                 // LED ngoài, đổi theo mạch của bạn
const uint16_t NGUONG_AP = 300;          // lực ấn tối thiểu theo số Z của XPT2046
const int SO_MAU = 7;                    // lấy trung vị 7 lần đọc
const uint32_t CHO_NHA_MS = 40;

TFT_eSPI tft;
Preferences boNho;

struct HeSo {
  float a, b, c, d, e, f;
};
HeSo heSo;

struct Nut {
  int16_t x, y, w, h;
  const char *chu;
  uint16_t mau;
};
Nut nutBat = {20, 150, 130, 70, "BAT", TFT_DARKGREEN};
Nut nutTat = {170, 150, 130, 70, "TAT", TFT_MAROON};
Nut nutChuan = {20, 20, 280, 40, "HIEU CHUAN", TFT_NAVY};

void veNut(const Nut &n, bool an) {
  tft.fillRoundRect(n.x, n.y, n.w, n.h, 8, an ? TFT_WHITE : n.mau);
  tft.drawRoundRect(n.x, n.y, n.w, n.h, 8, TFT_WHITE);
  tft.setTextDatum(MC_DATUM);
  tft.setTextColor(an ? TFT_BLACK : TFT_WHITE);
  tft.drawString(n.chu, n.x + n.w / 2, n.y + n.h / 2, 4);
}

bool trongNut(const Nut &n, int16_t x, int16_t y) {
  return x >= n.x && x < n.x + n.w && y >= n.y && y < n.y + n.h;
}

// Đọc số thô: bỏ qua nếu ấn nhẹ, lấy trung vị để loại các lần đọc lệch khi bút vừa chạm.
bool docTho(uint16_t &rx, uint16_t &ry) {
  uint16_t mx[SO_MAU], my[SO_MAU];
  for (int i = 0; i < SO_MAU; i++) {
    if (tft.getTouchRawZ() < NGUONG_AP) {
      return false;
    }
    tft.getTouchRaw(&mx[i], &my[i]);
  }
  for (int i = 1; i < SO_MAU; i++) {     // sắp xếp chèn, mảng rất ngắn
    for (int j = i; j > 0 && mx[j - 1] > mx[j]; j--) {
      const uint16_t t = mx[j]; mx[j] = mx[j - 1]; mx[j - 1] = t;
    }
    for (int j = i; j > 0 && my[j - 1] > my[j]; j--) {
      const uint16_t t = my[j]; my[j] = my[j - 1]; my[j - 1] = t;
    }
  }
  rx = mx[SO_MAU / 2];
  ry = my[SO_MAU / 2];
  return true;
}

// Chạm tại dấu cộng: lấy trung bình 16 lần đọc tốt rồi chờ nhả tay.
void doDiem(int16_t x, int16_t y, float &rx, float &ry) {
  tft.fillScreen(TFT_BLACK);
  tft.setTextDatum(MC_DATUM);
  tft.setTextColor(TFT_WHITE);
  tft.drawString("Cham dung tam dau cong", 160, 200 - (y > 120 ? 150 : 0), 2);
  tft.drawFastHLine(x - 12, y, 25, TFT_YELLOW);
  tft.drawFastVLine(x, y - 12, 25, TFT_YELLOW);
  uint32_t tongX = 0, tongY = 0;
  int dem = 0;
  while (dem < 16) {
    uint16_t a, b;
    if (docTho(a, b)) {
      tongX += a;
      tongY += b;
      dem++;
    }
    delay(10);
  }
  rx = tongX / 16.0f;
  ry = tongY / 16.0f;
  while (tft.getTouchRawZ() >= NGUONG_AP) {
    delay(10);
  }
  delay(300);
}

void tinhHeSo(const float x[3], const float y[3], const float r[3], const float s[3]) {
  const float d = (r[0] - r[2]) * (s[1] - s[2]) - (r[1] - r[2]) * (s[0] - s[2]);
  heSo.a = ((x[0] - x[2]) * (s[1] - s[2]) - (x[1] - x[2]) * (s[0] - s[2])) / d;
  heSo.b = ((r[0] - r[2]) * (x[1] - x[2]) - (r[1] - r[2]) * (x[0] - x[2])) / d;
  heSo.c = x[2] - heSo.a * r[2] - heSo.b * s[2];
  heSo.d = ((y[0] - y[2]) * (s[1] - s[2]) - (y[1] - y[2]) * (s[0] - s[2])) / d;
  heSo.e = ((r[0] - r[2]) * (y[1] - y[2]) - (r[1] - r[2]) * (y[0] - y[2])) / d;
  heSo.f = y[2] - heSo.d * r[2] - heSo.e * s[2];
}

void hieuChuan() {
  const float x[3] = {32, 288, 160};
  const float y[3] = {24, 120, 216};
  float r[3], s[3];
  for (int i = 0; i < 3; i++) {
    doDiem(x[i], y[i], r[i], s[i]);
  }
  tinhHeSo(x, y, r, s);
  boNho.putBytes("heso", &heSo, sizeof(heSo));
  Serial.printf("He so: %.5f %.5f %.2f  %.5f %.5f %.2f\n", heSo.a, heSo.b, heSo.c, heSo.d, heSo.e, heSo.f);
}

void veManHinh() {
  tft.fillScreen(TFT_BLACK);
  veNut(nutChuan, false);
  veNut(nutBat, false);
  veNut(nutTat, false);
}

void setup() {
  Serial.begin(115200);
  pinMode(CHAN_LED, OUTPUT);
  tft.init();
  tft.setRotation(1);                    // 320 x 240 nằm ngang
  boNho.begin("cam-ung", false);
  if (boNho.getBytes("heso", &heSo, sizeof(heSo)) != sizeof(heSo)) {
    hieuChuan();                         // chưa có số trong flash
  }
  veManHinh();
}

void loop() {
  uint16_t rx, ry;
  if (!docTho(rx, ry)) {
    return;
  }
  const int16_t x = heSo.a * rx + heSo.b * ry + heSo.c;
  const int16_t y = heSo.d * rx + heSo.e * ry + heSo.f;
  Serial.printf("tho %u %u -> diem %d %d\n", rx, ry, x, y);
  tft.fillCircle(x, y, 2, TFT_YELLOW);   // chấm vàng cho thấy điểm máy hiểu, dùng để kiểm tra độ lệch

  const Nut *nut = trongNut(nutBat, x, y) ? &nutBat : trongNut(nutTat, x, y) ? &nutTat : trongNut(nutChuan, x, y) ? &nutChuan : nullptr;
  if (nut) {
    veNut(*nut, true);
  }
  while (tft.getTouchRawZ() >= NGUONG_AP) {
    delay(10);                           // chờ nhả để mỗi lần ấn chỉ tính một lần
  }
  delay(CHO_NHA_MS);
  if (nut == &nutBat || nut == &nutTat) {
    digitalWrite(CHAN_LED, nut == &nutBat ? HIGH : LOW);
    veNut(*nut, false);
  } else if (nut == &nutChuan) {
    hieuChuan();
    veManHinh();
  }
}
Giao diện chương trình trên màn 320 x 240: nút HIEU CHUAN ở trên, nút BAT màu xanh và TAT màu đỏ ở dưới, các chấm vàng là điểm chạm máy tính ra
Hình vẽ lại theo tọa độ trong code.

Các điểm chính

  • docTho() bỏ qua khi lực ấn dưới ngưỡng, rồi lấy trung vị 7 lần đọc. Trung vị loại được các lần đọc lệch hẳn, còn trung bình thì bị chúng kéo theo.
  • tinhHeSo() giải 6 hệ số bằng công thức định thức. Đây là cùng công thức trong script mô phỏng.
  • Chấm vàng vẽ tại điểm máy tính ra. Chạm vào giữa một nút rồi nhìn chấm: lệch nhiều tức là cần hiệu chuẩn lại. Dự án thật thì bỏ dòng này.
  • Chờ nhả tay rồi mới thực hiện lệnh, giống nút thật, và mỗi lần ấn chỉ tính một lần. Nhờ vậy ấn giữ không làm nút chạy lặp.
  • Preferences lưu cả cấu trúc hệ số. Thay màn hoặc xoay hướng màn (setRotation) thì phải hiệu chuẩn lại.

TFT_eSPI có sẵn hàm tft.calibrateTouch() và tft.getTouch(), dùng rất tiện nếu không cần tự xử lý: chạy ví dụ Touch_calibrate để lấy mảng số rồi đưa vào tft.setTouch(). Thư viện XPT2046_Touchscreen của Paul Stoffregen là lựa chọn khi màn không dùng TFT_eSPI. Giao diện phức tạp hơn thì dùng LVGL, trong bài cùng cụm.

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

Hiện tượngNguyên nhânCách xử lý
Màn sáng nhưng cảm ứng không có tác dụngChưa nối T_DO, T_CS; chưa khai báo TOUCH_CSKiểm tra dây D19, D21 và file cấu hình
Số thô luôn 0 hoặc 4095Dây T_CLK, T_DIN sai; SPI quá nhanhKiểm tra dây, để SPI_TOUCH_FREQUENCY 2500000
Chạm chỗ này, nhảy chỗ khácChưa hiệu chuẩn, hoặc đổi setRotation sau khi hiệu chuẩnHiệu chuẩn lại ở đúng hướng màn
Điểm nhảy lung tung lúc nhả tayĐọc khi lực ấn yếuTăng NGUONG_AP, tăng SO_MAU
Nhiễu khi màn đang vẽHai thiết bị tranh SPI từ task khác nhauĐọc cảm ứng và vẽ trong cùng một task
Lệch ở một vùng sau thời gian dàiTấm cảm ứng bị xước, lão hóa hoặc mạch bị ép congKhông để vỏ hộp ép lên màn; chạy lại hiệu chuẩn

6. Xem thêm

Sản phẩm dùng trong bài: ESP32 DevKit V1 30 chân. Website chưa bán màn TFT cảm ứng.

Bài cùng cụm chủ đề: cấu hình TFT_eSPI; LVGL là gì; Cheap Yellow Display.


Tin tức liên quan

Hiệu chuẩn cảm biến laser đo khoảng cách và extensometer theo ISO 9513: bề mặt, cấp và sai số lệch
Hiệu chuẩn cảm biến laser đo khoảng cách và extensometer theo ISO 9513: bề mặt, cấp và sai số lệch

Hiệu chuẩn cảm biến laser tam giác lượng và extensometer: ảnh hưởng của bề mặt, trình tự trên bàn trượt, bảng cấp ISO 9513 với giới hạn tương đối và tuyệt đối, ví dụ extensometer 50 mm.

G-code là gì: các lệnh G0, G1, G2, G3, M3, M5 cơ bản và cách đọc một file G-code
G-code là gì: các lệnh G0, G1, G2, G3, M3, M5 cơ bản và cách đọc một file G-code

G-code là gì, các lệnh G0 G1 G2 G3 G90 G91 M3 M5 F S dùng nhiều nhất trên máy CNC, máy laser chạy GRBL, cung tròn I J, gốc phôi, kèm chương trình Python tự sinh G-code.

RS-485 là gì: cặp dây vi sai, điện trở đầu cuối và cách dùng module MAX485 với ESP32
RS-485 là gì: cặp dây vi sai, điện trở đầu cuối và cách dùng module MAX485 với ESP32

Giải thích chuẩn RS-485: tín hiệu vi sai trên hai dây A, B, điện trở đầu cuối 120 Ω, truyền bán song công với chân DE, RE. Kèm sơ đồ nối module MAX485 và chương trình hỏi đáp giữa hai mạch ESP32.

Chỉnh PID: Ziegler–Nichols, thử rơ le tìm Ku Tu, công thức SIMC và chỉnh tay theo đáp ứng
Chỉnh PID: Ziegler–Nichols, thử rơ le tìm Ku Tu, công thức SIMC và chỉnh tay theo đáp ứng

Cách chỉnh thông số PID: ảnh hưởng của Kp, Ki, Kd; Ziegler–Nichols bằng thử rơ le tìm Ku, Tu; công thức SIMC; so sánh trên mô hình lò nhiệt; chỉnh tay; ESP32 tự dò PID cho lò nhiệt dùng SSR.

Gia công xốp chống sốc cho đồ chơi mô hình RC, tay điều khiển và bộ sạc
Gia công xốp chống sốc cho đồ chơi mô hình RC, tay điều khiển và bộ sạc

Gia công xốp chống sốc cho đồ chơi mô hình đắt tiền: mẫu vali đựng tay điều khiển xe RC, bộ sạc và túi pin với năm lớp xốp cắt theo ba độ sâu, kèm lưu ý về pin và cách đặt làm.

Breadboard, dây cắm và hàn linh kiện: lắp mạch thử đúng cách cho người mới
Breadboard, dây cắm và hàn linh kiện: lắp mạch thử đúng cách cho người mới

Cách dùng breadboard: bên trong nối thế nào, cắm linh kiện và IC đúng cách, dây cắm, cấp nguồn và gắn ESP32; cách hàn linh kiện: dụng cụ, nhiệt độ, năm bước hàn, mối hàn tốt và xấu, an toàn.

Hiệu chuẩn cờ lê lực theo ISO 6789: phân loại, dung sai ±4 %, trình tự đo và lỗi khi sử dụng
Hiệu chuẩn cờ lê lực theo ISO 6789: phân loại, dung sai ±4 %, trình tự đo và lỗi khi sử dụng

Cờ lê lực theo ISO 6789-1 và ISO 6789-2: kiểu chỉ thị và kiểu cài đặt, dung sai ±4 %, trình tự hiệu chuẩn ba điểm mười lần đo, ảnh hưởng điểm đặt tay, đầu nối dài và cách bảo quản.

Nhận lệnh giọng nói không cần Internet trên ESP32-S3 bằng ESP_SR: Hi ESP, micro INMP441, đèn, quạt
Nhận lệnh giọng nói không cần Internet trên ESP32-S3 bằng ESP_SR: Hi ESP, micro INMP441, đèn, quạt

Nhận lệnh giọng nói ngay trên ESP32-S3 bằng thư viện ESP_SR có sẵn: từ đánh thức Hi ESP, lệnh tiếng Anh tự viết, micro INMP441, cài đặt phân vùng ESP SR 16M, bật tắt đèn và quạt.


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