Phát âm thanh trên ESP32: PWM, DAC 8 bit và I2S với MAX98357A, tạo sóng sin không cần thư viện

So sánh ba cách phát âm thanh trên ESP32: PWM, DAC 8 bit có DMA và I2S với MAX98357A; tạo sóng sin bằng bảng tra và bộ cộng pha, tránh tiếng tạch, phát giai điệu mẫu.

ESP32 phát được âm thanh theo ba cách, từ tiếng bíp đơn giản tới nhạc, giọng nói rõ ràng: PWM (xung vuông từ khối LEDC), DAC 8 bit có sẵn ở GPIO25, GPIO26, và I2S gửi âm thanh số tới mạch khuếch đại như MAX98357A. Bài này so sánh ba cách, giải thích cách tạo dạng sóng sin bằng bảng tra và bộ cộng pha, cách tránh tiếng "tạch" giữa các nốt, rồi viết một chương trình phát giai điệu theo cả ba cách, không cần thư viện ngoài.

1. Ba cách tạo âm thanh

Ba dạng sóng cùng nốt A4 440 Hz: PWM là xung vuông hai mức; DAC 8 bit là sóng sin dạng bậc thang mỗi bậc 50 micro giây; I2S 16 bit là sóng sin trơn
Dạng sóng ở chân ra của ESP32 khi phát nốt A4 (tính theo đúng cách chương trình tạo mẫu).
  • PWM: ledcWriteTone(chân, tần số) tạo xung vuông đúng tần số nốt. Chỉ có hai mức nên tiếng giống còi điện tử, nhưng cực kỳ đơn giản và chân nào cũng dùng được. Hợp với âm báo, giai điệu đơn.
  • DAC: ESP32 (bản gốc) có hai bộ chuyển đổi số – tương tự 8 bit ở GPIO25 và GPIO26, ra được điện áp 0–3,3 V với 256 mức. Gửi đều đặn các mẫu của dạng sóng là ra âm thanh bất kỳ, nhưng tín hiệu yếu, cần mạch khuếch đại, và 8 bit nghe hơi rè. Các dòng ESP32-S3, C3 không có DAC.
  • I2S: gửi mẫu 16 bit ở dạng số qua ba dây tới chip như MAX98357A, chip này vừa chuyển sang tương tự vừa khuếch đại (khoảng 3 W với loa 4 Ω). Âm thanh rõ nhất, ít nhiễu nhất, và có trên mọi dòng ESP32.

2. Tạo sóng sin bằng bảng tra và bộ cộng pha

Tính sin() cho từng mẫu (20 000 lần mỗi giây) thì tốn thời gian. Chương trình tính sẵn một bảng sin 256 điểm, rồi dùng một biến pha 32 bit: mỗi mẫu cộng thêm buoc = f × 232 / 20 000, và lấy 8 bit cao của pha làm chỉ số tra bảng. Biến pha tự quay vòng khi tràn, đúng như góc quay hết một vòng. Với nốt A4 (440 Hz), mỗi chu kỳ dài khoảng 45 mẫu.

Bật tắt nốt đột ngột tạo ra tiếng "tạch" khó chịu, vì sóng nhảy từ một giá trị bất kỳ về 0. Hàm heSoBao() cho âm lượng lên dần trong 5 ms đầu và xuống dần trong 5 ms cuối mỗi nốt, tiếng nghe mượt hẳn.

Với DAC, mẫu phải không âm: 128 là im lặng, sóng dao động quanh 128. Với I2S, mẫu là số 16 bit có dấu, mỗi khung gồm kênh trái và phải; chương trình ghi cùng một giá trị cho cả hai kênh.

3. Nối dây

Sơ đồ nối MAX98357A với ESP32 DevKit V1: LRC vào D14, BCLK vào D27, DIN vào D22, GAIN và SD để trống, GND vào GND, VIN vào VIN
VIN vào VIN (5 V), BCLK vào GPIO27, LRC vào GPIO14, DIN vào GPIO22; loa 4–8 Ω vào hai cọc + và − (hình minh họa).
Ba chuỗi khối: PWM từ ESP32 GPIO26 qua transistor NPN tới còi hoặc loa nhỏ; DAC từ GPIO25 qua tụ 10 µF tới mạch khuếch đại tương tự rồi loa; I2S từ ESP32 ba dây tới MAX98357A rồi loa 4 đến 8 ohm
Mỗi cách cần những khối khác nhau giữa ESP32 và loa (hình minh họa).
CáchChân ESP32Nối tới
1. PWMGPIO26Điện trở 1 kΩ vào base transistor NPN; còi hoặc loa nhỏ (nối tiếp điện trở 47–100 Ω) từ 5 V xuống collector
2. DACGPIO25Tụ 10 µF nối tiếp vào ngõ vào mạch khuếch đại (ví dụ PAM8403), GND chung
3. I2SGPIO27 (BCLK), GPIO14 (LRC), GPIO22 (DIN)MAX98357A: VIN 5 V, GND; loa vào hai cọc + và −

Không nối loa thẳng vào chân ESP32: loa 8 Ω cần dòng lớn hơn rất nhiều so với mức một chân GPIO chịu được. Với MAX98357A, hai cọc loa đều là ngõ ra (mạch cầu), không cọc nào nối GND. Chân GAIN để trống cho độ khuếch đại 9 dB; nối GND hoặc VIN để đổi mức.

4. Chương trình

Đặt hằng CACH_PHAT = 1, 2 hoặc 3 rồi nạp; gõ P trên Serial Monitor để phát giai điệu dân gian Frère Jacques (ở Việt Nam quen với lời "Kìa con bướm vàng"). Mỗi lần chỉ khởi động một cách, để DAC (dùng chung khối DMA với I2S trên ESP32) và I2S không giành nhau phần cứng.

// PYWORLD.VN - Phát âm thanh trên ESP32 theo 3 cách: xung PWM (LEDC), DAC 8 bit có DMA (GPIO25), I2S ra mạch khuếch đại MAX98357A
// Chọn cách bằng hằng CACH_PHAT rồi nạp lại; gõ P trên Serial Monitor để phát bài "Kìa con bướm vàng" (giai điệu dân gian Frère Jacques)
// Không cần thư viện ngoài: LEDC, driver DAC của ESP-IDF và thư viện ESP_I2S đều có trong gói esp32 3.x

#include <ESP_I2S.h>
#include <driver/dac_continuous.h>

const int CACH_PHAT = 3;                 // 1: PWM ở GPIO26, 2: DAC ở GPIO25, 3: I2S ra MAX98357A
const int CHAN_PWM = 26;
const int CHAN_BCLK = 27, CHAN_LRC = 14, CHAN_DIN = 22;   // nối với BCLK, LRC, DIN của MAX98357A
const uint32_t TAN_SO_MAU = 20000;       // 20 000 mẫu mỗi giây (DAC của ESP32 cần từ khoảng 19,6 kHz trở lên)
const int BIEN_DO_I2S = 8000;            // trên 32 767: khoảng 1/4 mức lớn nhất, đủ to với loa 3 W
const int BIEN_DO_DAC = 90;              // trên 127 (DAC 8 bit, giữa là 128)

struct Not { uint16_t tanSo, ms; };     // tần số 0 = nghỉ
const Not BAI_HAT[] = {
  {262, 400}, {294, 400}, {330, 400}, {262, 400}, {262, 400}, {294, 400}, {330, 400}, {262, 400},
  {330, 400}, {349, 400}, {392, 800}, {330, 400}, {349, 400}, {392, 800},
  {392, 200}, {440, 200}, {392, 200}, {349, 200}, {330, 400}, {262, 400},
  {392, 200}, {440, 200}, {392, 200}, {349, 200}, {330, 400}, {262, 400},
  {262, 400}, {196, 400}, {262, 800}, {262, 400}, {196, 400}, {262, 800},
};

I2SClass i2s;
dac_continuous_handle_t dac;

// Bảng sin 256 điểm, tính một lần; mẫu thứ n của nốt tần số f là bangSin[pha >> 24] với pha tăng f * 2^32 / TAN_SO_MAU mỗi mẫu.
int16_t bangSin[256];

void taoBangSin() {
  for (int i = 0; i < 256; i++) bangSin[i] = (int16_t)(32767 * sin(2 * PI * i / 256));
}

// Hệ số âm lượng 0..256 theo vị trí trong nốt: lên dần 5 ms đầu, xuống dần 5 ms cuối để không nghe tiếng "tạch".
int heSoBao(uint32_t n, uint32_t tong) {
  const uint32_t len = TAN_SO_MAU / 200;
  if (n < len) return n * 256 / len;
  if (tong - n < len) return (tong - n) * 256 / len;
  return 256;
}

void phatNotI2S(uint16_t f, uint32_t soMau) {
  static int16_t dem[512];               // 256 khung, mỗi khung 2 kênh (trái, phải giống nhau)
  uint32_t pha = 0, buoc = (uint32_t)((uint64_t)f * 4294967296ULL / TAN_SO_MAU);
  for (uint32_t n = 0; n < soMau;) {
    int k = 0;
    for (; k < 256 && n < soMau; k++, n++) {
      int32_t s = f ? (int32_t)bangSin[pha >> 24] * BIEN_DO_I2S / 32767 * heSoBao(n, soMau) / 256 : 0;
      dem[2 * k] = dem[2 * k + 1] = s;
      pha += buoc;
    }
    i2s.write((uint8_t *)dem, k * 4);    // chờ khi bộ đệm DMA đầy, nên nhịp phát do phần cứng giữ
  }
}

void phatNotDAC(uint16_t f, uint32_t soMau) {
  static uint8_t dem[512];
  uint32_t pha = 0, buoc = (uint32_t)((uint64_t)f * 4294967296ULL / TAN_SO_MAU);
  for (uint32_t n = 0; n < soMau;) {
    int k = 0;
    for (; k < 512 && n < soMau; k++, n++) {
      int s = f ? (int32_t)bangSin[pha >> 24] * BIEN_DO_DAC / 32767 * heSoBao(n, soMau) / 256 : 0;
      dem[k] = 128 + s;                  // DAC không có số âm: 0..255, im lặng là 128
      pha += buoc;
    }
    dac_continuous_write(dac, dem, k, NULL, -1);
  }
}

void phatBai() {
  for (const Not &nt : BAI_HAT) {
    uint32_t soMau = TAN_SO_MAU * nt.ms / 1000;
    if (CACH_PHAT == 1) {
      ledcWriteTone(CHAN_PWM, nt.tanSo);  // xung vuông đúng tần số nốt
      delay(nt.ms * 9 / 10);
      ledcWriteTone(CHAN_PWM, 0);         // ngắt một chút giữa hai nốt
      delay(nt.ms / 10);
    } else if (CACH_PHAT == 2) {
      phatNotDAC(nt.tanSo, soMau);
    } else {
      phatNotI2S(nt.tanSo, soMau);
    }
  }
  if (CACH_PHAT == 3) phatNotI2S(0, TAN_SO_MAU / 5);   // đẩy 0,2 s im lặng ra để nốt cuối phát hết
  if (CACH_PHAT == 2) phatNotDAC(0, TAN_SO_MAU / 5);
  Serial.println("Phat xong");
}

void setup() {
  Serial.begin(115200);
  taoBangSin();
  if (CACH_PHAT == 1) {
    ledcAttach(CHAN_PWM, 1000, 10);
    ledcWriteTone(CHAN_PWM, 0);
  } else if (CACH_PHAT == 2) {
    dac_continuous_config_t cauHinh = {
      .chan_mask = DAC_CHANNEL_MASK_CH0, // GPIO25
      .desc_num = 4,
      .buf_size = 1024,
      .freq_hz = TAN_SO_MAU,
      .offset = 0,
      .clk_src = DAC_DIGI_CLK_SRC_DEFAULT,
      .chan_mode = DAC_CHANNEL_MODE_SIMUL,
    };
    if (dac_continuous_new_channels(&cauHinh, &dac) != ESP_OK || dac_continuous_enable(dac) != ESP_OK) {
      Serial.println("Khong khoi dong duoc DAC");
    }
  } else {
    i2s.setPins(CHAN_BCLK, CHAN_LRC, CHAN_DIN);
    if (!i2s.begin(I2S_MODE_STD, TAN_SO_MAU, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO)) {
      Serial.println("Khong khoi dong duoc I2S");
    }
  }
  const char *ten[] = {"", "PWM GPIO26", "DAC GPIO25", "I2S MAX98357A"};
  Serial.printf("Cach phat: %s, %lu mau/giay. Go P de phat\n", ten[CACH_PHAT], (unsigned long)TAN_SO_MAU);
}

void loop() {
  if (Serial.available() && toupper(Serial.read()) == 'P') {
    Serial.println("Dang phat...");
    phatBai();
  }
}
  • Cách 2 dùng trình điều khiển dac_continuous của ESP-IDF (có sẵn trong gói esp32 3.x): mẫu được DMA đẩy ra đúng 20 000 lần mỗi giây, chương trình chỉ cần ghi đầy bộ đệm. DAC của ESP32 cần tần số lấy mẫu từ khoảng 19,6 kHz trở lên với nguồn xung mặc định, nên bài dùng 20 kHz.
  • Cách 3 dùng thư viện ESP_I2S: i2s.write() chờ khi bộ đệm DMA đầy, nên nhịp phát do phần cứng giữ, chương trình không phải tự canh giờ.
  • Cuối bài phát thêm 0,2 giây im lặng để đẩy hết mẫu còn trong bộ đệm ra loa.

5. Kết quả mong đợi

Biểu đồ các nốt theo thời gian từ G3 196 Hz tới A4 440 Hz, mỗi nốt là một ô màu xanh, tổng khoảng 12,8 giây
Mảng BAI_HAT: mỗi ô là một nốt (tần số, thời gian); giai điệu dân gian Frère Jacques.
Serial Monitor: cách phát I2S MAX98357A 20000 mẫu mỗi giây, gõ P để phát; Dang phat; Phat xong
Gõ P: chương trình phát 32 nốt trong khoảng 13 giây.

Nghe lần lượt ba cách sẽ thấy rõ khác biệt: PWM vang, chói như còi; DAC tròn tiếng hơn nhưng có tiếng xì nền; I2S với MAX98357A rõ và to nhất. Muốn phát file âm thanh, mỗi mẫu trong file WAV chính là một số 16 bit để gửi qua I2S như trong hàm phatNotI2S(); file nhỏ lưu được trong LittleFS, file lớn nên để trên thẻ nhớ.

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

Hiện tượngNguyên nhân thường gặpCách xử lý
MAX98357A không kêuNối nhầm BCLK và LRC; thiếu nguồn VIN; loa hỏngĐối chiếu bảng nối dây; đo 5 V ở VIN
Tiếng rè, méo khi toBiên độ quá lớn, nguồn 5 V yếuGiảm BIEN_DO_I2S; nguồn 5 V đủ dòng (tới 1 A khi to)
Tiếng "tạch" giữa các nốtBỏ phần lên dần, xuống dầnGiữ heSoBao()
Nhạc phát nhanh hoặc chậm hơnTần số lấy mẫu khai báo khác tần số thậtDùng cùng TAN_SO_MAU khi tạo mẫu và khi khởi động I2S, DAC
"Khong khoi dong duoc DAC"Tần số lấy mẫu dưới mức DAC hỗ trợ; dùng chip không có DACGiữ 20 kHz trở lên; ESP32-S3, C3 dùng I2S
Loa PWM kêu nhỏ, transistor nóngLoa nối thẳng không có điện trở hạn dòngThêm điện trở 47–100 Ω nối tiếp loa

7. Xem thêm

  • PWM trên ESP32: khối LEDC dùng cho cách 1.
  • Đàn piano trái cây với ESP32: dự án STEM phát nốt nhạc bằng còi.
  • Bài micro I2S INMP441 trong loạt module phổ biến: chiều ngược lại, thu âm thanh qua I2S.
  • Bài LittleFS trong cùng loạt ESP32 nâng cao: lưu file âm thanh nhỏ trong flash.

Tin tức liên quan

Mạch điều khiển bay của flycam: IMU, vòng PID, bộ lọc rung, chế độ bay, Betaflight, ArduPilot
Mạch điều khiển bay của flycam: IMU, vòng PID, bộ lọc rung, chế độ bay, Betaflight, ArduPilot

Mạch điều khiển bay của flycam: MCU, IMU, cảm biến, vòng điều khiển nhiều tầng, bộ trộn động cơ, PID, bộ lọc rung, các chế độ bay, Betaflight, INAV, ArduPilot, PX4 và khả năng dùng ESP32.

Mạch tăng áp XL6009: từ 3,7 V hoặc 5 V lên 12 V, dòng ra được bao nhiêu, chỉnh áp và lỗi hay gặp
Mạch tăng áp XL6009: từ 3,7 V hoặc 5 V lên 12 V, dòng ra được bao nhiêu, chỉnh áp và lỗi hay gặp

Mạch tăng áp XL6009 hoạt động thế nào, thông số IC và module, cách ước tính dòng ra thực tế khi nâng pin 18650, nguồn USB 5 V, ắc quy 12 V lên 12 V, 24 V, chỉnh áp và lỗi hay gặp.

Cảm biến áp suất: cấu tạo, nguyên lý áp trở, điện dung, áp điện và cách chế tạo
Cảm biến áp suất: cấu tạo, nguyên lý áp trở, điện dung, áp điện và cách chế tạo

Cảm biến áp suất hoạt động thế nào: áp suất tuyệt đối và tương đối, màng và ống Bourdon, áp trở silic, điện dung, áp điện, quy trình chế tạo MEMS và cách đọc loại 0,5–4,5 V bằng ESP32.

Hiệu chỉnh độ chính xác máy CNC, máy laser tự chế: bước/mm, vuông góc, độ rơ, phay phẳng, dò Z
Hiệu chỉnh độ chính xác máy CNC, máy laser tự chế: bước/mm, vuông góc, độ rơ, phay phẳng, dò Z

Đo và hiệu chỉnh máy CNC, máy laser tự chế: số bước mỗi milimét, độ vuông góc bằng hai đường chéo, độ rơ bằng đồng hồ so, phay phẳng tấm lót, dò Z bằng G38.2, kèm chương trình Python.

Xúc xắc điện tử với ESP32 và OLED: thí nghiệm xác suất cho học sinh
Xúc xắc điện tử với ESP32 và OLED: thí nghiệm xác suất cho học sinh

Làm xúc xắc điện tử bằng ESP32 và màn hình OLED kèm 4 nút: tung một lần, tung nhanh 60 lần, vẽ biểu đồ; xác suất lý thuyết, thực nghiệm và luật số lớn cho học sinh.

Hiệu chuẩn load cell tại hiện trường: tải thật, mô phỏng mV/V, thế chỗ, ISO 376 và ASTM E74
Hiệu chuẩn load cell tại hiện trường: tải thật, mô phỏng mV/V, thế chỗ, ISO 376 và ASTM E74

Hiệu chuẩn hệ load cell tại hiện trường: chuỗi đo và nguồn sai số, tải thật so với mô phỏng mV/V, thế chỗ cho cân bồn, lưu ý lắp đặt, so sánh ISO 376 và ASTM E74.

Module relay 1, 2, 4 kênh: kích mức thấp hay cao, opto cách ly, jumper JD-VCC, chọn 5 V hay 12 V
Module relay 1, 2, 4 kênh: kích mức thấp hay cao, opto cách ly, jumper JD-VCC, chọn 5 V hay 12 V

Module relay 1, 2, 4 kênh: bên trong một kênh có opto, kích mức thấp hay mức cao, jumper JD-VCC để cách ly với ESP32, chọn bản 5 V hay 12 V và đấu tải 220 V qua COM, NO an toàn.

An toàn khi dùng máy khắc laser và máy CNC: cấp laser, kính bảo hộ, khói độc, cháy và mũi phay
An toàn khi dùng máy khắc laser và máy CNC: cấp laser, kính bảo hộ, khói độc, cháy và mũi phay

Những điều phải biết trước khi bật máy khắc laser và máy CNC: cấp laser, chọn kính bảo hộ theo bước sóng và OD, vật liệu cấm khắc, phòng cháy, hút khói, an toàn dao phay và điện.


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