Micro I2S INMP441 với ESP32: đo mức âm thanh dBFS, dB SPL và vỗ tay hai lần bật tắt đèn

Khung I2S của micro INMP441, nối với ESP32, đổi số đo 24 bit ra dBFS và ước tính dB SPL, nhận tiếng vỗ tay bằng mức nền và độ vọt, bật tắt đèn khi vỗ tay hai lần.

INMP441 là micro MEMS nhỏ cỡ hạt gạo, có sẵn bộ chuyển đổi tương tự – số 24 bit và gửi âm thanh ra theo chuẩn số I2S. Khác với micro điện dung có mạch khuếch đại (ra điện áp tương tự, đọc bằng ADC), INMP441 nối thẳng vào khối I2S của ESP32 nên ít nhiễu và không cần chỉnh biến trở. Bài này giải thích khung I2S, cách đổi số đo ra dBFS và ước tính dB SPL, rồi viết chương trình đo mức âm thanh và nhận tiếng vỗ tay hai lần để bật tắt đèn, chỉ dùng thư viện có sẵn trong gói esp32.

1. I2S và khung dữ liệu của INMP441

Giản đồ thời gian I2S: đường WS mức thấp là kênh trái, mức cao là kênh phải; đường SCK 64 xung mỗi mẫu; đường SD chứa 24 bit dữ liệu ở đầu khe kênh trái, khe kênh phải để thả nổi; ví dụ byte FF F3 A2 C0 đổi ra -3166 và -68,5 dBFS
Mỗi mẫu 24 bit nằm ở đầu khe 32 bit của kênh trái (hình minh họa).

I2S là chuẩn truyền âm thanh số dùng ba dây:

  • SCK (xung bit): ESP32 phát ra, mỗi xung một bit. INMP441 cần 64 xung cho mỗi mẫu (32 cho mỗi kênh), nên ở 16 000 mẫu/giây SCK chạy 1,024 MHz.
  • WS (chọn kênh): mức thấp là kênh trái, mức cao là kênh phải. Tần số của WS chính là tần số lấy mẫu.
  • SD (dữ liệu): micro gửi 24 bit, bit cao trước, bắt đầu một xung sau khi WS đổi mức.

Chân L/R của INMP441 chọn kênh micro gửi: nối GND thì gửi ở kênh trái, nối 3,3 V thì gửi ở kênh phải. Hai micro có L/R khác nhau dùng chung SCK, WS, SD được, thành micro stereo. Trong khe còn lại, micro thả nổi chân SD.

Chương trình đọc mỗi kênh thành một số 32 bit. 24 bit dữ liệu nằm ở phần cao, nên dịch phải có dấu 8 bit sẽ được số 24 bit, từ −8 388 608 tới 8 388 607. Chương trình đọc cả hai kênh (stereo) rồi tự chọn kênh có tín hiệu: kênh không có micro đọc ra số gần như không đổi, kênh có micro luôn có chút nhiễu nền. Cách này tránh được lỗi đảo kênh trái/phải và không phụ thuộc chân L/R được nối thế nào.

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

3. Nối INMP441 với ESP32

Sơ đồ nối module INMP441 với ESP32 DevKit V1: VDD vào 3V3, GND và L/R vào GND, WS vào D25, SCK vào D26, SD vào D33
VDD vào 3V3; SCK vào GPIO26, WS vào GPIO25, SD vào GPIO33; L/R nối GND (hình minh họa).
INMP441ESP32 DevKit V1Ghi chú
VDD3V3Không cấp 5 V
GNDGND
L/RGNDMicro gửi ở kênh trái; không để hở chân này
WSD25
SCKD26
SDD33Dữ liệu vào ESP32

Khối I2S của ESP32 gán được vào hầu hết các chân, nên có thể đổi ba hằng CHAN_SCK, CHAN_WS, CHAN_SD. Tránh GPIO34–39 cho SCK và WS (chỉ nhận vào), tránh GPIO6–11 (nối bộ nhớ flash). Giữ dây SCK, WS, SD ngắn: xung 1 MHz qua dây dài dễ méo, đọc sai bit.

4. dBFS và dB SPL: đổi số đo ra mức âm thanh

Thang đo hai cột dBFS và dB SPL từ 0 dBFS bằng 120 dB SPL xuống -90 dBFS bằng 30 dB SPL, các mốc: mức lớn nhất, tiếng vỗ tay gần micro, mốc độ nhạy 94 dB SPL bằng -26 dBFS, nói chuyện cách 1 m, phòng yên tĩnh, nhiễu riêng của micro
Thang của số đo (dBFS) và mức áp suất âm thanh (dB SPL) theo độ nhạy của INMP441 (giá trị điển hình).

Mỗi khối 256 mẫu (16 ms), chương trình bỏ phần một chiều, tính giá trị hiệu dụng (căn của trung bình bình phương) rồi đổi ra decibel so với mức lớn nhất:

dBFS = 20 × log10(hiệu dụng / 8 388 608)

dBFS luôn âm hoặc bằng 0: 0 dBFS là micro đã đạt mức lớn nhất. Datasheet INMP441 cho biết tiếng ở 94 dB SPL (áp suất 1 Pa) cho ra −26 dBFS, nên dB SPL ≈ dBFS + 120. Con số này chỉ là ước tính: mỗi micro lệch vài dB, không lọc theo tai người (trọng số A) như máy đo độ ồn, và vỏ hộp, lỗ thu âm cũng làm thay đổi kết quả. Muốn so độ ồn giữa các nơi thì đủ dùng; muốn đo đúng theo quy chuẩn thì phải dùng máy đo đã kiểm định.

Nhiễu riêng của INMP441 vào khoảng 33 dBA (khoảng −87 dBFS), nên phòng thật yên tĩnh sẽ đọc ra gần mức đó chứ không thấp hơn.

5. Nhận tiếng vỗ tay

Hai đồ thị trong hơn 4 giây: trên là dạng sóng với đoạn tiếng nói nhỏ và ba tiếng vỗ tay nhọn; dưới là mức từng khối 16 ms theo dBFS, đường mức nền quanh -62, đường nền cộng 20 dB, tiếng nói lên khoảng -34 theo ba âm tiết, ba chấm đỏ ở đỉnh tiếng vỗ tay khoảng -20
Tiếng nói rồi hai tiếng vỗ cách nhau 0,45 giây và một tiếng vỗ đơn (âm thanh mô phỏng).

Tiếng vỗ tay khác tiếng nói ở chỗ nó nổ lên tức thì rồi tắt trong vài chục mili giây; tiếng nói lên dần qua vài khối. Chương trình dùng ba điều kiện:

  1. Mức nền là đường đi theo lúc yên tĩnh: hạ nhanh khi âm thanh nhỏ đi, tăng rất chậm khi có tiếng (mỗi khối chỉ nhích 0,5% khoảng cách), nên tiếng nói hay tiếng vỗ không kéo nền lên ngay.
  2. Một khối được coi là tiếng vỗ khi nó cao hơn khối trước ít nhất 12 dB và cao hơn nền ít nhất 20 dB.
  3. Sau một tiếng vỗ, bỏ qua 150 ms để tiếng vọng không bị đếm thêm.

Để bật tắt đèn, cần hai tiếng vỗ cách nhau 0,2–0,8 giây. Tiếng cửa đóng, tiếng gõ bàn, âm "p", "t" nói sát micro cũng có thể vọt lên đủ nhanh để bị nhận là một tiếng vỗ; yêu cầu hai tiếng liên tiếp đúng nhịp giảm hẳn những lần bật nhầm như vậy.

6. Chương trình

Chương trình dùng thư viện ESP_I2S có trong gói esp32 3.x, không cần cài thêm. Đặt VE_DO_THI = true để in mức âm và mức nền cho Serial Plotter, tiện chỉnh hai ngưỡng 12 dB, 20 dB theo căn phòng.

// PYWORLD.VN - Micro I2S INMP441 với ESP32: đo mức âm thanh (dBFS, ước tính dB SPL) và vỗ tay hai lần để bật tắt đèn
// Nối: VDD 3V3, GND, L/R xuống GND, SCK vào GPIO26, WS vào GPIO25, SD vào GPIO33. Đèn xanh trên bo (GPIO2) là đèn được bật tắt
// Chỉ dùng thư viện ESP_I2S có sẵn trong gói esp32

#include <ESP_I2S.h>

const int CHAN_SCK = 26, CHAN_WS = 25, CHAN_SD = 33;
const int CHAN_DEN = 2;
const uint32_t TAN_SO_MAU = 16000;
const int SO_MAU_KHOI = 256;                 // mỗi khối 16 ms
const bool VE_DO_THI = false;                // true: in mức âm cho Serial Plotter
const float DBFS_SANG_SPL = 120.0;           // datasheet: 94 dB SPL cho ra -26 dBFS → cộng 120

I2SClass i2s;
int32_t dem[SO_MAU_KHOI * 2];                // khung stereo: trái, phải, trái, phải...
int kenh = 0;                                // 0: trái (L/R nối GND), 1: phải (L/R nối 3V3)

float dc = 0;                                // phần một chiều của micro
float nen = -60, dbTruoc = -90;              // mức nền theo dõi chậm, mức của khối trước (dBFS)
uint32_t lanVoTruoc = 0, lanIn = 0;
bool choLanHai = false, den = false;

int docKhoi() {
  size_t n = i2s.readBytes((char *)dem, sizeof(dem));
  return n / 8;                              // 8 byte cho mỗi khung (2 kênh × 32 bit)
}

// Kênh không có micro đọc ra số không đổi; kênh có micro luôn có nhiễu nền: chọn kênh dao động nhiều hơn.
void chonKenh() {
  double tong[2] = {0, 0}, tong2[2] = {0, 0};
  int dem2 = 0;
  for (int k = 0; k < 20; k++) {
    int n = docKhoi();
    for (int i = 0; i < n; i++) {
      for (int c = 0; c < 2; c++) {
        double x = dem[2 * i + c] >> 8;
        tong[c] += x;
        tong2[c] += x * x;
      }
    }
    dem2 += n;
  }
  if (dem2 == 0) return;
  double ps0 = tong2[0] / dem2 - pow(tong[0] / dem2, 2), ps1 = tong2[1] / dem2 - pow(tong[1] / dem2, 2);
  kenh = ps1 > ps0 ? 1 : 0;
}

void voTay(uint32_t bayGio) {
  if (choLanHai && bayGio - lanVoTruoc >= 200 && bayGio - lanVoTruoc <= 800) {
    den = !den;                              // lần vỗ thứ hai trong 0,2–0,8 giây: đảo trạng thái đèn
    digitalWrite(CHAN_DEN, den);
    choLanHai = false;
    if (!VE_DO_THI) Serial.printf("Vo tay 2 lan: den %s\n", den ? "BAT" : "TAT");
  } else {
    choLanHai = true;
    if (!VE_DO_THI) Serial.println("Vo tay 1 lan, cho lan thu hai...");
  }
  lanVoTruoc = bayGio;
}

void setup() {
  Serial.begin(115200);
  pinMode(CHAN_DEN, OUTPUT);
  i2s.setPins(CHAN_SCK, CHAN_WS, -1, CHAN_SD);
  if (!i2s.begin(I2S_MODE_STD, TAN_SO_MAU, I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO)) {
    Serial.println("Khong khoi dong duoc I2S");
    while (true) delay(1000);
  }
  chonKenh();
  if (!VE_DO_THI) Serial.printf("INMP441 san sang, doc kenh %s. Vo tay 2 lan de bat tat den\n", kenh ? "phai" : "trai");
}

void loop() {
  int n = docKhoi();
  if (n == 0) return;
  double tong2 = 0;
  for (int i = 0; i < n; i++) {
    float x = dem[2 * i + kenh] >> 8;        // 24 bit có dấu nằm ở 24 bit cao của khung 32 bit
    dc += (x - dc) * 0.001f;                 // trung bình trượt rất chậm: phần một chiều
    float y = x - dc;
    tong2 += (double)y * y;
  }
  float rms = sqrt(tong2 / n);
  float dbfs = 20 * log10f(rms / 8388608.0f + 1e-9f);   // so với mức lớn nhất 2^23
  if (dbfs < -100) dbfs = -100;              // khối toàn số 0 lúc mới chạy: không để mức nền tụt quá sâu

  // Mức nền: xuống nhanh khi yên tĩnh, lên rất chậm khi có tiếng (để tiếng nói, tiếng vỗ không kéo nền lên ngay)
  nen += (dbfs - nen) * (dbfs < nen ? 0.1f : 0.005f);

  uint32_t bayGio = millis();
  // Tiếng vỗ tay: vọt lên ít nhất 12 dB so với khối trước (16 ms) và cao hơn nền 20 dB
  if (dbfs - dbTruoc >= 12 && dbfs - nen >= 20 && bayGio - lanVoTruoc > 150) voTay(bayGio);
  if (choLanHai && bayGio - lanVoTruoc > 800) choLanHai = false;
  dbTruoc = dbfs;

  if (VE_DO_THI) {
    Serial.printf("muc:%.1f,nen:%.1f\n", dbfs, nen);
  } else if (bayGio - lanIn >= 500) {
    lanIn = bayGio;
    Serial.printf("Muc am %.1f dBFS (khoang %.0f dB SPL) | nen %.1f dBFS\n", dbfs, dbfs + DBFS_SANG_SPL, nen);
  }
}
  • i2s.begin(I2S_MODE_STD, 16000, I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO): chuẩn I2S Philips, 32 bit mỗi kênh, hai kênh. Bộ đệm dem chứa xen kẽ trái, phải.
  • chonKenh() đọc 20 khối đầu, tính phương sai từng kênh và chọn kênh lớn hơn. Các khối đầu tiên sau khi khởi động có thể toàn số 0, cũng được bỏ qua ở bước này.
  • Phần một chiều được bỏ bằng trung bình trượt rất chậm (dc += (x − dc) × 0,001); mức dưới −100 dBFS bị chặn lại để mức nền không tụt quá sâu khi gặp khối toàn số 0.
  • voTay() giữ trạng thái "đang chờ lần hai"; quá 0,8 giây không có lần hai thì bỏ.

7. Kết quả mong đợi

Serial Monitor: các dòng mức âm khoảng -62 dBFS, khoảng 58 dB SPL khi yên tĩnh, -34 dBFS khi nói gần micro; dòng vỗ tay 1 lần chờ lần thứ hai, rồi vỗ tay 2 lần đèn BẬT; một tiếng vỗ đơn cuối không đổi trạng thái đèn
Mỗi 0,5 giây một dòng mức âm; dòng vỗ tay in ngay khi nhận được (âm thanh mô phỏng).

Phòng làm việc bình thường thường đọc ra khoảng 40–55 dB SPL; số trong hình là ví dụ. Vỗ tay hai lần cách micro khoảng 1 m, đèn xanh trên bo ESP32 đổi trạng thái. Muốn điều khiển đèn thật, nối module relay vào một chân khác và xem cách đấu an toàn trong bài ESP32 điều khiển module relay.

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

Hiện tượngNguyên nhân thường gặpCách xử lý
Mức âm luôn −100 dBFSSai chân SD; micro không có nguồn; nối nhầm SCK với WSKiểm tra lại 6 dây; đo 3,3 V ở VDD
Mức âm gần 0 dBFS liên tục, nghe như nhiễu trắngLệch nhịp I2S: dây quá dài, SCK và WS đảoDây ngắn; đổi lại SCK, WS
Mức âm không đổi khi nói toĐọc nhầm kênh trống; lỗ thu âm bị dán kínNối chắc L/R xuống GND rồi khởi động lại; để hở lỗ thu âm
Vỗ tay không nhậnPhòng ồn, nền cao; vỗ xa microBật VE_DO_THI xem mức; hạ ngưỡng 20 dB xuống 15 dB
Đèn tự bật tắt khi nói chuyệnÂm bật hơi sát micro; tiếng gõ bànĐặt micro xa người nói; tăng ngưỡng 12 dB lên 15 dB
"Khong khoi dong duoc I2S"Chân đã dùng cho việc khác, chân chỉ nhận vàoChọn chân khác cho SCK, WS

9. Xem thêm


Tin tức liên quan

Mạch hạ áp LM2596 và MP1584: cách chọn, chỉnh điện áp và cấp nguồn ESP32
Mạch hạ áp LM2596 và MP1584: cách chọn, chỉnh điện áp và cấp nguồn ESP32

So sánh mạch hạ áp LM2596 và MP1584, cách chỉnh điện áp ra bằng đồng hồ vạn năng trước khi nối tải, cấp nguồn 5 V cho ESP32 DevKit V1 từ nguồn 12 V và xử lý lỗi thường gặp.

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

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.

Hướng dẫn bắt đầu MicroPython trên ESP32 Lite với Thonny
Hướng dẫn bắt đầu MicroPython trên ESP32 Lite với Thonny

Hướng dẫn cài driver, cài Thonny và nạp firmware MicroPython cho mạch ESP32 Lite, dùng REPL, nháy LED, lưu main.py, đọc nút nhấn, quét I2C và xử lý lỗi thường gặp.

Cấu hình LoRa trên ESP32: SF, băng thông, tỉ lệ mã, công suất và cách thử tầm xa
Cấu hình LoRa trên ESP32: SF, băng thông, tỉ lệ mã, công suất và cách thử tầm xa

Giải thích bốn tham số cấu hình LoRa (SF, băng thông, tỉ lệ mã, công suất), cách tính thời gian phát một gói và hướng dẫn làm bộ thử tầm xa bằng ESP32, SX1278 và màn hình OLED.

Hướng dẫn dùng PWM trên ESP32: điều chỉnh độ sáng LED bằng analogWrite và LEDC
Hướng dẫn dùng PWM trên ESP32: điều chỉnh độ sáng LED bằng analogWrite và LEDC

Hướng dẫn tạo PWM trên ESP32 bằng Arduino IDE: analogWrite, ledcAttach và ledcWrite của gói esp32 3.x, chỉnh độ sáng LED bằng biến trở, chọn tần số và độ phân giải.

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

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

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 điều khiển servo SG90 bằng PWM, không cần thư viện: quét góc, biến trở, hiệu chỉnh
ESP32 điều khiển servo SG90 bằng PWM, không cần thư viện: quét góc, biến trở, hiệu chỉnh

Hướng dẫn điều khiển servo SG90 bằng ESP32 với bộ PWM LEDC 50 Hz có sẵn: xung 0,5–2,4 ms, sơ đồ nối, quét góc, vặn biến trở, hiệu chỉnh hai đầu hành trình và thí nghiệm STEM.


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