Loa trợ lý AI trên ESP32-S3: cấu tạo phần cứng, cách hoạt động, độ trễ, quyền riêng tư, thử âm thanh
Loa trợ lý AI tự làm bằng ESP32-S3 hoạt động thế nào: micro I2S, mạch khuếch đại, phần chạy trên chip và trên máy chủ, vì sao có độ trễ, dữ liệu đi đâu, chương trình thử ghi âm, phát lại.
Những chiếc loa trợ lý AI nhỏ gọn tự làm từ ESP32-S3 ngày càng phổ biến: nói "Hi ESP", hỏi một câu bằng tiếng Việt, vài giây sau loa trả lời bằng giọng nói. Một vi điều khiển vài trăm KB RAM làm sao "trò chuyện" được? Câu trả lời: loa chỉ làm phần nghe, gửi và phát; phần hiểu câu và nghĩ câu trả lời nằm ở máy chủ. Bài này giải thích cấu tạo phần cứng, luồng xử lý một câu hỏi, vì sao có độ trễ, dữ liệu đi đâu, và kèm chương trình thử khối âm thanh (ghi âm từ micro, phát lại qua loa) trên ESP32-S3.
1. Cấu tạo phần cứng

- ESP32-S3 có PSRAM (ví dụ N16R8): Wi-Fi, đủ RAM để đệm âm thanh, chạy được từ đánh thức tại chỗ.
- Micro I2S INMP441: micro số, nối thẳng vào chip, ít nhiễu hơn micro analog.
- Mạch khuếch đại I2S MAX98357A và loa 4 Ω, 3 W: nhận âm thanh số qua I2S, không cần DAC rời.
- Nút, đèn LED (hoặc màn hình nhỏ): bấm để nói, báo trạng thái đang nghe, đang nghĩ.
- Nguồn: USB 5 V, hoặc pin 18650 với mạch sạc và tăng áp. Loa phát to kéo dòng đỉnh lớn: nguồn yếu làm chip khởi động lại.
2. Một câu hỏi đi qua những đâu

- Từ đánh thức chạy ngay trên ESP32-S3 (như bài nhận lệnh giọng nói bằng ESP-SR cùng loạt): loa không gửi gì đi khi chưa nghe thấy.
- Ghi âm và nén: âm thanh 16 kHz, 16 bit là 256 kbit/s; nén (ví dụ chuẩn Opus dành cho giọng nói) còn vài chục kbit/s, nhẹ cho Wi-Fi.
- Gửi lên máy chủ qua một kết nối giữ liên tục (thường là WebSocket).
- Máy chủ: nhận dạng tiếng nói thành chữ, đưa vào mô hình ngôn ngữ để có câu trả lời, rồi tổng hợp thành giọng nói. Tiếng Việt được hỗ trợ ở phía máy chủ, không phụ thuộc vào chip.
- Phát: loa nhận âm thanh, giải nén, phát qua mạch khuếch đại.
3. Vì sao phải chờ vài giây

Mỗi bước cộng thêm thời gian chờ. Các thiết kế tốt rút ngắn bằng cách gửi âm thanh liên tục trong lúc người còn nói, máy chủ trả lời từng đoạn và loa phát ngay đoạn đầu, chọn máy chủ ở gần, giữ Wi-Fi ổn định.
4. Dữ liệu đi đâu

Đặt đèn báo khi loa đang gửi âm thanh, có nút tắt micro, và chọn dịch vụ máy chủ có chính sách dữ liệu rõ ràng. Khóa dịch vụ không nên ghi thẳng trong loa (xem phần bảo vệ khóa trong bài ESP32 gọi API mô hình ngôn ngữ cùng loạt).
5. Thử khối âm thanh: ghi âm và phát lại
Trước khi ghép cả hệ thống, nên thử riêng "tai" và "miệng" của loa. Nối micro INMP441: SCK GPIO4, WS GPIO5, SD GPIO6, VDD 3V3, GND, L/R nối GND. Nối MAX98357A: BCLK GPIO15, LRC GPIO16, DIN GPIO7, VIN 5V, GND; hai dây loa vào cổng ra. Chương trình chỉ dùng thư viện ESP_I2S có sẵn trong gói ESP32; trong Arduino IDE chọn bo ESP32-S3 và bật PSRAM.
// PYWORLD.VN - Khối âm thanh của loa trợ lý trên ESP32-S3: giữ nút BOOT để ghi âm từ micro INMP441, thả nút để phát lại
// PYWORLD.VN - Hai bộ I2S: một nhận từ micro (16 kHz), một phát ra mạch khuếch đại MAX98357A; tự chỉnh âm lượng trước khi phát
// PYWORLD.VN - Micro: SCK GPIO4, WS GPIO5, SD GPIO6; loa: BCLK GPIO15, LRC GPIO16, DIN GPIO7
#include "ESP_I2S.h"
const int CHAN_NUT = 0; // nút BOOT, nhấn = mức thấp
const int MIC_SCK = 4, MIC_WS = 5, MIC_SD = 6;
const int LOA_BCLK = 15, LOA_LRC = 16, LOA_DIN = 7;
const uint32_t TAN_SO = 16000;
const uint32_t GIAY_TOI_DA = 5;
const size_t SO_MAU_TOI_DA = TAN_SO * GIAY_TOI_DA;
const int32_t DINH_MONG_MUON = 20000; // biên độ đích sau khi chỉnh âm lượng (tối đa 32767)
const float HE_SO_TOI_DA = 8.0f;
I2SClass mic;
I2SClass loa;
int16_t *boDem = nullptr;
void setup() {
Serial.begin(115200);
pinMode(CHAN_NUT, INPUT_PULLUP);
boDem = (int16_t *)ps_malloc(SO_MAU_TOI_DA * sizeof(int16_t)); // ưu tiên PSRAM
if (!boDem) boDem = (int16_t *)malloc(SO_MAU_TOI_DA * sizeof(int16_t));
if (!boDem) {
Serial.println("Khong du bo nho cho bo dem am thanh");
while (true) delay(1000);
}
mic.setPins(MIC_SCK, MIC_WS, -1, MIC_SD);
mic.begin(I2S_MODE_STD, TAN_SO, I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO, I2S_STD_SLOT_LEFT);
mic.configureRX(TAN_SO, I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO, I2S_RX_TRANSFORM_32_TO_16);
loa.setPins(LOA_BCLK, LOA_LRC, LOA_DIN);
loa.begin(I2S_MODE_STD, TAN_SO, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO);
Serial.println("Giu nut BOOT de noi, tha ra de nghe lai (toi da 5 giay)");
}
void loop() {
if (digitalRead(CHAN_NUT) == HIGH) {
delay(10);
return;
}
// Ghi âm trong lúc giữ nút
size_t daGhi = 0;
while (digitalRead(CHAN_NUT) == LOW && daGhi < SO_MAU_TOI_DA) {
size_t conLai = SO_MAU_TOI_DA - daGhi;
size_t lanNay = conLai < 512 ? conLai : 512;
size_t soByte = mic.readBytes((char *)(boDem + daGhi), lanNay * sizeof(int16_t));
daGhi += soByte / sizeof(int16_t);
}
// Tìm đỉnh, tự chỉnh âm lượng để giọng nói đủ to mà không bị méo
int32_t dinh = 0;
for (size_t i = 0; i < daGhi; i++) {
int32_t a = abs((int32_t)boDem[i]);
if (a > dinh) dinh = a;
}
float heSo = dinh > 0 ? (float)DINH_MONG_MUON / dinh : 1.0f;
if (heSo > HE_SO_TOI_DA) heSo = HE_SO_TOI_DA;
for (size_t i = 0; i < daGhi; i++) {
int32_t v = (int32_t)(boDem[i] * heSo);
boDem[i] = (int16_t)(v > 32767 ? 32767 : (v < -32768 ? -32768 : v));
}
Serial.printf("Ghi %.2f giay, dinh %ld, he so am luong %.1f\n", daGhi / (float)TAN_SO, (long)dinh, heSo);
loa.write((uint8_t *)boDem, daGhi * sizeof(int16_t));
Serial.println("Phat lai xong");
while (digitalRead(CHAN_NUT) == LOW) delay(10);
}

- Hai bộ I2S chạy cùng lúc: một nhận từ micro, một phát ra mạch khuếch đại.
- Bộ đệm 5 giây (160 KB) đặt trong PSRAM nếu có.
- Chương trình tự chỉnh âm lượng: tìm biên độ lớn nhất của đoạn ghi, nhân lên để đỉnh đạt khoảng 20000 (tối đa 8 lần), tránh méo tiếng.
- Nghe lại rõ, không rè: micro và loa đã sẵn sàng cho bước tiếp theo.
6. Ghép thành loa hoàn chỉnh
- Thêm từ đánh thức bằng ESP_SR thay cho nút bấm.
- Thêm phần nén âm thanh và kết nối WebSocket tới máy chủ; máy chủ gọi các dịch vụ nhận dạng tiếng nói, mô hình ngôn ngữ, tổng hợp tiếng nói.
- Cách nhanh hơn: dùng một dự án mã nguồn mở dựng sẵn cho loa trợ lý ESP32-S3 (có cả chương trình cho loa và phần máy chủ). Chọn dự án còn được duy trì, đọc kỹ chính sách dữ liệu và chi phí dịch vụ đi kèm.
7. Lỗi hay gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Tiếng hú, rè khi vừa nghe vừa phát | Micro thu lại tiếng của chính loa | Đặt micro xa loa, hướng khác nhau; dùng khử vọng âm (AEC) với kênh tham chiếu |
| Phát rất nhỏ | Chân GAIN của MAX98357A, loa trở kháng cao | Đổi cách nối GAIN theo tài liệu; dùng loa 4 Ω |
| Chip khởi động lại khi phát to | Nguồn yếu, dây USB dài | Nguồn 5 V đủ dòng, thêm tụ lớn gần mạch khuếch đại |
| Ghi được toàn số 0 | Chân L/R chưa nối, sai chân SD/WS | Nối L/R vào GND, kiểm tra dây |
| Báo không đủ bộ nhớ | Chưa bật PSRAM | Bật PSRAM đúng loại của bo |
8. Xem thêm
- Nguồn pin: mô-đun pin 18650 ra 5 V, khay pin 18650; lắp thử với dây cắm.
- Các bài cùng loạt AI trên vi điều khiển: nhận lệnh giọng nói bằng ESP-SR, ESP32 gọi API mô hình ngôn ngữ, TinyML là gì; bài micro INMP441 trong loạt module, bài phát âm thanh I2S trong loạt ESP32 nâng cao.
Xem thêm