
Audio를 주제로 한 첫 번째 실험은 바로 소형 스피커로 소리 내기 입니다.
atomic14 유튜버의 "ESP32 Audio Output with I2S DMA and the MAX98357A Class D Amplifier" 영상을 메인으로 참고해서 실습을 진행했습니다.
2개 8Ω 1W 2030 스피커 (알리익스프레스 참고) - 가격 2,260 원

스피커 스펙을 보면 "8Ω 1W" 라고 나와 있는데 무슨 의미일까요? 이것은 아무래도 스피커를 선택하는데 중요한 특징인 거 같습니다.
스피커의 전기적 저항을 나타내는 단위로, 옴(Ω)을 사용하며 임피던스라고 불립니다. 참고로, 임피던스는 교류 신호에 대한 저항이며 직류 저항과는 차이가 있습니다.
임피던스는 앰프와 스피커를 연결할 때 중요한 요소이며, 일반적으로 앰프의 출력 임피던스와 스피커의 임피던스를 맞춰주는 것이 좋아요. 4옴과 8옴이 가장 일반적인 스피커 임피던스 입니다.
📌 옴(임피던스)이 크면
📌 옴(임피던스)이 작으면
스피커의 허용 입력 전력을 의미합니다. 스피커가 손상되지 않고 견딜 수 있는 최대 전력량을 나타냅니다. 1W는 아주 작은 출력처럼 보일 수 있지만, 일반적인 가정 환경에서 충분한 소리를 내기에 충분해요.
주의할 점은 1W는 스피커를 구동하는 데 필요한 최소한의 전력이 아니라, 스피커가 손상되지 않고 안전하게 받아들일 수 있는 최대 전력이라는 점입니다.
🎯 전력과 전류 계산 (1W, 8Ω 스피커의 경우)
이 계산으로 ESP32가 왜 스피커를 직접 못 돌리는지 수치적으로 명확히 알 수 있습니다. (오디오 증폭기가 필요한 이유)
ESP32는 최대 40mA, 3.3V만 출력 가능한데 스피커는 354mA가 필요하므로, 약 9배 부족한 전류 때문에 직접 구동이 불가능하고 앰프가 필수입니다.
만약 ESP32에 스피커를 앰프 없이 직접 연결하면 다음과 같은 결과가 나타납니다.
결론: 들리긴 하지만 실용성 제로 + 하드웨어 손상 위험이므로 반드시 앰프를 사용해야 합니다!
스피커는 소형/초소형(부저, 알람, 이어폰, 소형 기기), 중형(블루투스 스피커, 라디오), 대형 스피커(데시크탑, 모니터, 카 오디오), 전문용/고급(무대용, 콘서트홀) 스피커로 나뉩니다.
| 임피던스 | 전력 | 용도 | 크기 |
|---|---|---|---|
| 8Ω 0.5W | 소형 | 부저 대용 | 28-40mm |
| 16Ω 0.25W | 초소형 | 이어폰, 헤드셋 | 15-20mm |
| 4Ω 3W | 중간 | 블루투스 스피커 | 50-70mm |
| 8Ω 5W | 대형 | 데스크탑 스피커 | 80-100mm |
따라서 스피커를 구매할 때는 임피던스와 전력을 참고해서 선택하길 용도에 적합한 스피커를 선택하길 바랍니다. 저는 간단한 테스트 용도라서 8Ω 1W 스피커가 적당하네요.
신호 증폭: 약한 오디오 신호(mV~V 수준)를 스피커가 필요로 하는 강한 신호(수V~수십V)로 전압과 전류를 동시에 증폭하여 충분한 전력을 공급합니다.
임피던스 매칭: 신호원(ESP32 등)의 높은 출력 임피던스와 스피커의 낮은 임피던스 사이를 중재하여 최적의 전력 전달과 효율적인 에너지 변환을 가능하게 합니다.
신호 품질 개선: 노이즈 제거, 왜곡 감소, 주파수 응답 보정 등을 통해 깨끗하고 선명한 음질로 스피커를 구동하며, 과전류나 단락으로부터 시스템을 보호하는 역할도 합니다.
"디지털 입력 앰프"와 "아날로그 입력 앰프"는 신호를 어디서 받아들이는지와 어떤 역할까지 맡는지에서 차이가 납니다.
아날로그 입력 앰프 (예: PAM8403, TPA3116 등)
디지털 입력 앰프 (예: MAX98357A, TAS5713 등)
저는 ESP32에서 디지털 오디오 데이터(I²S)를 직접 스피커로 출력하고 싶기 때문에 디지털 입력 앰프(MAX98357A)를 사용해야 합니다.
MAX98357A 오디오 앰프(증폭기) 모듈 (알리익스프레이스 참고) - 가격 1,956 원

MAX98357A 오디오 앰프 모듈은 디지털 오디오 신호를 스피커를 구동할 수 있는 아날로그 신호로 변환하고 증폭하는 역할을 해요. 이 모듈은 특히 마이크로컨트롤러(예: Arduino, Raspberry Pi)와 스피커 사이에서 인터페이스 역할을 담당합니다.
주요 기능 및 특징은 다음과 같습니다.
MAX98357A 제품 사양은 다음과 같습니다.
게인(Gain)은 입력되는 오디오 신호의 볼륨을 얼마나 증폭시킬지를 결정하는 값입니다.
게인이 낮으면 입력 신호를 조금만 증폭시킵니다. 소리가 작아지지만, 음질이 깨끗하고 왜곡이 적습니다. 반대로 게인이 높으면 입력 신호를 많이 증폭시킵니다. 소리가 커지지만, 너무 높으면 소리가 찌그러지거나(클리핑), 잡음이 같이 증폭되어 음질이 나빠질 수 있습니다.
MAX98357A 모듈은 게인을 하드웨어적으로 설정할 수 있습니다. 모듈에 있는 GAIN 핀을 어떤 상태로 연결하느냐에 따라 게인 값이 달라집니다.

아무것도 연결하지 않으면 9 dB, 약 2.8배 출력 신호 증폭 비율을 가집니다. 진폭 비율 공식을 통해 2.8배 라는 것을 계산할 수 있습니다.

모노(Monaural)는 소리를 하나의 채널에 담는 방식입니다. 단일 채널이며 소리의 모든 정보(악기, 목소리 등)가 하나의 오디오 신호에 통합되어 있습니다. 따라서 음향 공간감이 느껴지지 않고, 소리의 위치를 구분할 수 없습니다. 단순히 소리가 "있다/없다"만을 표현합니다.
스테레오(Stereophonic)는 소리를 두 개 이상의 채널에 담는 방식입니다. 일반적으로는 왼쪽(Left)과 오른쪽(Right)의 두 채널을 사용합니다. 좌우 채널에 다른 소리 정보를 담아 소리의 공간감과 입체감을 만들어냅니다.
앰프가 (L+R)/2 스테레오 믹스를 모노 출력으로 변환한다는 것은 다음과 같은 의미입니다.
이렇게 하는 이유는 스테레오 신호를 모노 스피커에 그대로 보내면 소리가 제대로 출력되지 않거나, 일부 소리가 상쇄되어 사라질 수 있기 때문입니다. 따라서 좌우 채널을 합쳐 하나의 완전한 모노 신호를 만들어 출력하는 것입니다.
🤔 음... 그러면 (L+R)/2 믹스는 별로 안 좋은거네요? 결국 모노 잖아요...
네 맞습니다. 앰프의 (L+R)/2 믹스는 스테레오의 입체감을 포기하고 모노로 변환하는 방식이기 때문에, 음향적인 관점에서는 '별로 안 좋은' 방식이라고 볼 수 있습니다. 그럼에도 불구하고 (L+R)/2 믹스를 사용하는 데는 명확한 이유가 있습니다. 당신이 앰프에 연결할 스피커가 한 개뿐이라면 이 믹스 기능은 사용할 수 밖에 없습니다. 모노 환경에서 스테레오 음원을 안정적으로 재생하기 위한 타협인 셈이죠.
만약 스테레오 방식을 사용하려고 한다면 MAX98357A 모듈 2개 + 스피커 2개, 하나는 왼쪽 채널용, 또 다른 하나는 오른쪽 채널용으로 만들어서 사용하면 됩니다. 동일한 I2S 신호를 이용해 두 개의 앰프를 독립적으로 구동함으로써 완전한 스테레오 시스템을 구축하는 것이죠.
THD는 오디오 신호가 증폭기나 기타 장치를 통과할 때, 원래의 깨끗한 신호에 추가되는 불필요한 고조파 성분의 양을 백분율로 나타낸 값입니다.
이상적인 증폭기는 입력된 신호의 파형을 변형 없이 그대로 증폭시켜 출력합니다. 하지만 현실에서는 회로의 비선형적인 특성으로 인해 원래의 주파수(기본 주파수) 외에 정수 배의 주파수를 가진 새로운 신호(고조파)가 생성됩니다.
10% THD라는 것은, 출력 신호의 전체 전력 중 고조파 성분이 차지하는 비율이 10%라는 것을 의미합니다. 소리가 찌그러지거나 부자연스럽게 들릴 수 있습니다. 반면 1% THD라는 것은 출력 신호의 1%가 왜곡된 성분으로, 매우 깨끗한 음질을 제공합니다.
10% THD는 오디오 품질 측면에서 매우 높은 수치입니다. 일반적으로 고음질 오디오 장비는 0.1% 이하의 THD를 목표로 하며, 1%만 되어도 사람의 귀로 왜곡을 감지할 수 있습니다. 10% THD는 음질이 상당히 떨어지고, 소리가 '깨진다'고 느껴질 정도의 높은 왜곡을 의미합니다.
그렇다면 아래 내용은 무슨 의미인걸까?
5V into 8Ω @ 10% THD - 1.8W max
5V into 8Ω @ 1% THD - 1.4W max
이 내용은 오디오 앰프의 최대 출력 전력(Pout)과 음질 왜곡(THD) 사이의 관계를 보여주는 스펙입니다. 출력 전력이 높아질수록 음질 왜곡이 심해진다는 것입니다. 따라서, 이 스펙은 앰프 제조사가 "이 앰프는 1.8W까지 낼 수 있지만, 좋은 음질을 원하면 1.4W 이하로 사용하세요"라고 권장하는 것과 같습니다.
실제 사용 시에는 우리가 소프트웨어적으로 볼륨을 조절하여 원하는 전력과 음질 수준을 선택해야 합니다. 음원의 볼륨을 적절하게 조절하여, 앰프의 출력 전력이 1.4W를 넘지 않도록 합니다.
Class D Amplifier(클래스 D 증폭기)는 오디오 신호를 증폭하는 방식 중 하나입니다. 기존의 아날로그 증폭기(A, B, AB 클래스)와 달리, 디지털 신호 처리 기술을 사용하여 높은 효율과 낮은 전력 소모를 달성하는 것이 가장 큰 특징입니다.
클래스 D 증폭기는 오디오 신호를 직접 증폭하지 않습니다. 대신, 다음 핵심 단계를 거칩니다.
다음은 PWM 신호를 생성하는 방법을 보여줍니다. 입력 사인파가 있습니다. PWM 신호를 생성하기 위해 입력 신호를 고주파 삼각파(캐리어)와 비교합니다. 입력 신호가 삼각파보다 높으면 HIGH(1), 낮으면 LOW(0)를 출력하여, 입력 신호의 크기에 비례해서 펄스의 폭(duty cycle)이 변합니다.

이렇게 생성된 PWM 신호는 원래 오디오 신호의 정보를 펄스 폭에 담고 있어서, 저역 통과 필터를 거치면 다시 원래의 아날로그 오디오 파형으로 복원되며, 이 과정에서 스위칭을 통한 전력 증폭이 이루어집니다.
즉, 입력 신호는 PWM 신호로 변환된 다음, PWM 신호를 저역 통과 필터링하여 출력을 재구성합니다. 여기서는 PWM 신호에 매우 낮은 주파수를 사용하고 있으므로 재구성된 신호에 노이즈가 많습니다.

Class D Amplifier 장점
Class D Amplifier 단점
앰프와 스피커를 연결할 때는 앰프의 허용 임피던스 범위 내에서 스피커를 선택하는 것이 가장 중요해요. 일반적으로 앰프는 4옴, 8옴, 16옴 등 지원하는 임피던스 값이 명시되어 있어요. 스피커의 옴 수가 앰프의 권장 옴 수보다 낮으면 앰프에 과부하가 걸릴 수 있으므로 주의해야 합니다.
이 앰프는 4-8옴 이라고 나와 있습니다. 이 범위 내에서 스피커를 선택해야 합니다.

스피커 임피던스가 앰프 권장값보다 낮으면 위험합니다. 4Ω 전용 앰프에 2Ω 스피커를 연결하면 과도한 전류가 흘러서 앰프가 과열되거나 손상될 수 있습니다 (I = V/R에서 R이 작을수록 I가 커짐).
반대로 임피던스가 높은 것은 상대적으로 안전하지만 출력이 줄어듭니다. 8Ω 앰프에 16Ω 스피커를 연결하면 앰프 손상 위험은 없지만 절반 정도의 출력만 나와서 소리가 작아집니다.
따라서 MAX98357A 앰프는 8옴 1W 스피커, 4옴 3W 스피커를 사용하기에 적합합니다.
I2S(Inter-IC Sound)는 IC(집적 회로) 간에 디지털 오디오 데이터를 전송하기 위한 통신 프로토콜이에요. 마이크로컨트롤러(ESP32 등)와 오디오 장치(DAC, 앰프, 코덱) 간의 표준 인터페이스로 사용됩니다.
🤔 왜 I2S 라는 프로토콜을 만들게 되었을까요?
I2S 프로토콜은 디지털 오디오 데이터를 전송하기 위해 만들어진 것입니다. 기존의 아날로그 방식이 아닌 디지털 방식으로 오디오 신호를 주고받아 잡음과 신호 왜곡을 최소화하기 위한 목적이 가장 큽니다.
아날로그 신호 변환 없이 디지털 상태로 오디오를 전송하므로 노이즈가 거의 없고, PCM 형태의 디지털 오디오 데이터를 실시간으로 스트리밍할 수 있어 ESP32 같은 MCU에서 고음질 오디오 출력에 최적입니다.
I2S는 데이터를 전송하기 위해 최소 3개의 라인을 사용해요.
SD라고 표시된 핀을 직렬 데이터 핀과 혼동하지 않도록 주의하세요. 이 핀은 실제로는 셧다운 및 채널 선택 핀입니다. (나 처음에 낚였잖아...) - adafruit 참고
SD 핀은 연결하지 않으면 브레이크아웃 보드 내부적으로 풀업 저항이 있어서 "기본적으로 앰프는 (L+R)/2 스테레오 믹스를 모노 출력으로 출력합니다".

SD 핀을 접지에 연결하면 앰프가 꺼집니다. VIN에 연결하면 앰프가 왼쪽 채널을 재생합니다. 그리고 오른쪽 채널을 재생하려면 풀업 저항을 사용해야 합니다. 이 저항 값은 보드에 이미 이 핀에 내부 전압 분배기가 있다는 사실 때문에 약간 복잡합니다.
여기서는 3.3V와 5V 모두에 작동하는 39킬로옴이라는 적절한 저항 값을 계산해 두었습니다. 하지만 안전을 위해 5V 전원 공급 장치를 사용하는 경우 저항 허용 오차를 고려하여 47킬로옴 저항을 사용하는 것이 좋을 수 있습니다.
I2S 3개 신호가의 동작 흐름을 살펴보면 다음과 같습니다. (이미지 출처)

일단, 이 정도만 알아두고 넘어가시죠. 밑에서 I2S 설정 및 실습, 오실로스코프 측정을 해보면 더 명확하게 알 수 있을테니까요.
I2S DMA(Direct Memory Access) 설정은 ESP32가 CPU의 개입 없이 메모리에서 I2S 주변 장치로 직접 오디오 데이터를 전송하게 하는 중요한 과정입니다. DMA를 사용하면 CPU는 다른 작업을 수행할 수 있어 오디오 재생 중에도 시스템의 안정성과 효율성이 높아집니다.

DMA 더블 버퍼링
I2S DMA는 더블 버퍼링을 사용하여 연속적인 오디오 스트리밍을 보장합니다 - 두 개의 버퍼(A, B)를 번갈아 사용하여 하나는 데이터를 전송하는 동안 다른 하나는 새로운 데이터로 채우는 방식입니다.
버퍼 A가 스피커로 전송되는 동안 CPU는 버퍼 B에 다음 오디오 데이터를 준비하고, 전송이 완료되면 역할을 바꿔서 끊김 없는 재생이 가능합니다 - DMA 인터럽트가 발생할 때마다 버퍼를 교체합니다.
이 방식으로 실시간 오디오 처리에서 언더런(underrun)이나 오버런(overrun) 없이 안정적인 44.1kHz 샘플링과 같은 고속 데이터 전송을 수행할 수 있으며, ESP32의 I2S 하드웨어가 이를 자동으로 지원합니다.
ESP32와 MAX98357A, 8옴 1W 스피커를 연결하는 방법은 다음과 같습니다. (이미지 출처)

MAX98357A는 I2S 인터페이스를 사용하여 ESP32와 통신을 합니다. I2S는 클럭(BCLK), 데이터(DIN), 워드 셀렉트(LRC) 세 가지 주요 핀으로 구성됩니다.
ESP32 ↔ MAX98357A
MAX98357A ↔ 스피커
ESP32에는 I2S 전용 핀이 따로 정해져 있지 않습니다. 대신, GPIO 매트릭스라는 기능을 사용하여 거의 모든 GPIO 핀을 I2S 기능에 할당할 수 있습니다.
일반적으로는 개발 보드의 사용 편의성과 충돌을 피하기 위해 특정 GPIO 핀을 권장하기도 하지만, 이는 필수적인 것은 아니며, 프로젝트의 필요에 따라 유연하게 핀을 선택할 수 있습니다.

ESP-IDF 환경에서 MAX98357A 앰프와 스피커를 테스트하는 간단한 예제 코드를 작성해봅니다. 정현파(sine wave) 데이터를 만들어서 소리로 출력해보는 예제입니다.
#include <stdio.h>
#include <math.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2s.h"
#include "driver/gpio.h"
// I2S 핀 설정 (GPIO 매트릭스에 따라 자유롭게 변경 가능)
#define I2S_BCLK_PIN GPIO_NUM_26
#define I2S_LRC_PIN GPIO_NUM_25
#define I2S_DOUT_PIN GPIO_NUM_22
// I2S 설정
#define I2S_NUM I2S_NUM_0
#define SAMPLE_RATE 44100
#define SAMPLES_PER_CYCLE 128
#define SINE_WAVE_AMPLITUDE 20000 // 16-bit signed, -32768 to 32767
// 정현파 데이터 버퍼
int16_t sine_wave[SAMPLES_PER_CYCLE];
void create_sine_wave() {
for (int i = 0; i < SAMPLES_PER_CYCLE; i++) {
// 정현파를 16비트 부호 있는 정수 형태로 생성
sine_wave[i] = (int16_t)(SINE_WAVE_AMPLITUDE * sin((2.0 * M_PI * i) / SAMPLES_PER_CYCLE));
}
}
void i2s_init() {
i2s_config_t i2s_config = {
.mode = I2S_MODE_MASTER | I2S_MODE_TX,
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 8,
.dma_buf_len = 64,
.use_apll = true
};
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_BCLK_PIN,
.ws_io_num = I2S_LRC_PIN,
.data_out_io_num = I2S_DOUT_PIN,
.data_in_io_num = I2S_PIN_NO_CHANGE
};
// I2S 드라이버 설치
i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
// 핀 설정
i2s_set_pin(I2S_NUM, &pin_config);
}
void app_main(void) {
create_sine_wave();
i2s_init();
while (1) {
size_t bytes_written;
// I2S에 데이터 쓰기 (스테레오 출력을 위해 데이터를 두 번 반복)
i2s_write(I2S_NUM, sine_wave, sizeof(sine_wave), &bytes_written, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
ESP32의 I2S 기능을 사용하여 정현파(sine wave)를 생성하고 이를 앰프를 통해 스피커로 출력합니다. 이를 통해 하드웨어 연결이 제대로 되었는지 확인할 수 있습니다.
i2s_config_t 구조체는 ESP32의 I2S 주변 장치를 설정하는 데 사용되며, 오디오 데이터의 형식과 전송 방식을 정의하는 중요한 역할을 합니다.
i2s_config_t i2s_config = {
.mode = I2S_MODE_MASTER | I2S_MODE_TX,
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 8,
.dma_buf_len = 64,
.use_apll = true
};
📌 mode: I2S 장치의 작동 모드를 결정합니다.
📌 sample_rate: 1초에 전송되는 오디오 샘플의 수입니다. CD 음질에 해당하는 44.1kHz는 일반적으로 고품질 오디오를 재생하는 데 사용됩니다.
오디오 샘플레이트(Sample Rate) 는 아날로그 소리를 디지털 신호로 변환할 때 초당 몇 번의 '샘플'(표본, 데이터 조각)을 추출하는지를 나타내는 수치입니다. 샘플레이트가 높을수록 더 많은 정보를 캡처하여 오디오 품질이 향상되지만, 데이터 용량도 커집니다.
📌 bits_per_sample: 하나의 오디오 샘플을 표현하는 데 사용되는 비트 수를 결정합니다. 16비트는 오디오의 정밀도(해상도)를 나타내며, 가장 일반적으로 사용되는 값입니다.
'Audio bit per sample'는 '샘플 1개당 비트 수'를 의미하며, 디지털 오디오에서 소리의 '진폭 해상도'를 나타내는 비트 심도(Bit Depth)의 다른 표현입니다. 비트 심도가 높을수록 각 샘플이 더 많은 단계의 진폭 값을 표현할 수 있어, 소리의 미세한 변화를 더 정확하게 기록하여 음질을 향상시키고 원본 아날로그 신호의 정밀도를 높입니다. (이미지 참고)

📌 channel_format: 스테레오(Stereo) 오디오에서 좌우 채널 데이터가 어떻게 전송되는지 정의합니다. I2S_CHANNEL_FMT_RIGHT_LEFT는 한 비트 클럭 동안 왼쪽 채널 데이터를 보내고, 다음 비트 클럭 동안 오른쪽 채널 데이터를 보내는 방식입니다.
WS/LRCK 신호가 'HIGH'일 때는 오른쪽 채널 데이터가 전송됩니다. WS/LRCK 신호가 'LOW'일 때느 왼쪽 채널 데이터가 전송됩니다. (이미지 참고)

bits_per_sample을 16bit로 설정했으면 16bit 동안은 Left 채널 데이터를 보내고, 다음 16bit 동안은 Right 채널 데이터를 보낸다고 볼 수 있겠네요.
📌 communication_format: I2S_COMM_FORMAT_STAND_I2S는 I2S 통신 프로토콜의 표준 형식을 따르도록 지정합니다. 이는 대부분의 I2S 장치와 호환되는 가장 일반적인 설정입니다.
📌 intr_alloc_flags: I2S 인터럽트가 할당될 때의 우선순위를 설정합니다. LEVEL1은 낮은 우선순위로, 다른 중요한 시스템 작업에 방해가 되지 않도록 합니다.
📌 dma_buf_count 및 dma_buf_le: I2S 통신에 사용되는 DMA(Direct Memory Access) 버퍼의 개수와 각 버퍼의 길이를 설정합니다. DMA는 CPU의 개입 없이 메모리에서 주변 장치로 직접 데이터를 전송하는 기술로, 끊김 없는 오디오 재생에 필수적입니다.
📌 use_apll: I2S 클럭 소스로 오디오 전용 PLL(Phase-Locked Loop)인 APLL을 사용하도록 설정합니다. APLL은 일반적인 CPU 클럭보다 오디오에 더 적합한 정밀한 클럭을 생성하여 음질을 향상시키는 데 도움이 됩니다.
dma_buf_count는 DMA 버퍼의 총 개수를 지정합니다. 버퍼가 많을수록 오디오 데이터의 흐름이 끊기지 않고 부드러워지지만, 그만큼 메모리 사용량도 늘어납니다.
dma_buf_len는 각 DMA 버퍼에 저장되는 샘플의 수를 지정합니다. dma_buf_len이 64이면, 각 버퍼에 64개의 오디오 샘플이 저장됩니다. 16비트 스테레오(2채널)의 경우, 하나의 샘플은 4바이트(16비트 * 2채널)이므로 한 버퍼의 크기는 64(개) x 4(바이트) = 256 바이트가 됩니다.
설정 시 고려사항
dma_buf_count와 dma_buf_len의 값은 시스템 메모리(RAM) 사용량에 영향을 미칩니다. 버퍼의 총 크기는 dma_buf_count × dma_buf_len × bytes_per_sample로 계산됩니다.
버퍼의 크기가 너무 작으면 underflow (데이터 부족)가 발생하여 소리가 끊길 수 있고, 너무 크면 오디오 재생 시작 시 약간의 지연이 발생할 수 있습니다. 프로젝트의 요구 사항에 맞춰 최적의 값을 찾는 것이 중요합니다.
예를 들어서, 44.1KHz, 16bit , 스테레오 채널 오디오를 생각해봅시다.
1초당 샘플 개수는 44,100개 이며, 샘플 하나를 표현하는 데 16비트의 데이터를 사용합니다. 스테레오 채널이므로 두 개의 채널(좌측, 우측)을 사용하므로 데이터 양을 계산할 때 2배를 곱해줘야 합니다.

이 값은 CD 음질 오디오의 1초당 용량과 동일하며, 이를 '176.4KB/s' 또는 '1.4Mbps'라고 부르기도 합니다. 이는 압축되지 않은 PCM(Pulse-Code Modulation) 오디오 데이터의 용량을 의미하며, MP3 같은 압축 포맷은 이보다 훨씬 작습니다.
DMA 버퍼 설정이 dma_buf_count = 8이고 dma_buf_len = 64일 때 메모리 사용량을 계산해보겠습니다.

버퍼 개수가 8이고, 각 버퍼의 길이가 64(샘플)이며, 16비트 스테레오 오디오의 경우, 한 샘플은 16비트(2바이트) × 2채널(스테레오) = 4바이트입니다.

따라서, dma_buf_count = 8, dma_buf_len = 64로 설정하면 총 2,048 바이트(약 2KB)의 RAM을 DMA 버퍼로 사용하게 됩니다. ESP32는 수십 KB에서 수백 KB의 RAM을 가지고 있기 때문에, 이 정도의 메모리 사용량은 전체 시스템에 큰 부담을 주지 않고 안정적인 오디오 재생에 충분합니다.
예제 워크플로우 (오디오 출력)
API 참고: https://docs.espressif.com/projects/esp-idf/en/v4.2/esp32/api-reference/peripherals/i2s.html
📌 i2s_driver_install: 이 함수는 I2S 통신을 위한 기본적인 환경을 설정합니다.
/**
- i2s_num: 사용할 I2S 포트 번호입니다. ESP32는 보통 I2S_NUM_0만 지원합니다.
- i2s_config: 앞서 설명한 i2s_config_t 구조체의 포인터로, 오디오 샘플링 속도, 비트 수, 채널 형식 등을 정의합니다.
- dma_buf_count: DMA 버퍼의 개수입니다. 이 함수가 메모리에 할당할 DMA 버퍼의 총 개수를 지정합니다.
- event_queue: 이벤트 큐 핸들을 저장할 포인터입니다. 오디오 스트리밍 이벤트(예: 데이터 부족)를 처리할 때 사용되지만, 간단한 예제에서는 NULL로 설정할 수 있습니다.
**/
esp_err_t i2s_driver_install(i2s_port_t i2s_num, const i2s_config_t *i2s_config, int dma_buf_count, i2s_event_queue_handle_t *event_queue);
이 함수를 호출하면 ESP32 칩 내부의 I2S 주변 장치를 사용할 수 있도록 준비하고, DMA 버퍼에 필요한 RAM을 예약합니다.
📌 i2s_set_pin: 이 함수는 I2S 드라이버와 실제 하드웨어 핀을 연결합니다.
/**
- i2s_num: I2S 포트 번호입니다. i2s_driver_install()에서 사용한 것과 동일해야 합니다.
- pin_config: i2s_pin_config_t 구조체의 포인터로, I2S 신호에 할당할 GPIO 핀 번호를 정의합니다.
**/
esp_err_t i2s_set_pin(i2s_port_t i2s_num, const i2s_pin_config_t *pin_config);
항상 i2s_driver_install()을 먼저 호출하여 드라이버와 버퍼를 준비한 다음, i2s_set_pin()을 호출하여 핀 설정을 완료해야 합니다. 이 순서가 바뀌면 올바르게 작동하지 않습니다.
📌 i2s_write: ESP32의 I2S 드라이버를 통해 오디오 데이터를 DMA 버퍼에 쓰는 함수입니다. 이 함수는 오디오 데이터를 I2S 주변 장치로 전송하는 데 사용됩니다.
/**
- i2s_port_t i2s_num: 사용할 I2S 포트 번호입니다. 보통 I2S_NUM_0으로 설정합니다.
- const void *src: 전송할 오디오 데이터가 저장된 버퍼의 포인터입니다. 이 버퍼에는 정현파, WAV 파일 등 실제 오디오 데이터가 들어있어야 합니다.
- size_t size: src 버퍼에 있는 데이터의 총 크기(바이트)입니다.
- size_t *bytes_written: 함수가 성공적으로 I2S 드라이버에 쓴 바이트 수를 반환하는 변수의 포인터입니다.
- TickType_t ticks_to_wait: 데이터가 버퍼에 쓰여지기를 기다리는 최대 시간(틱)입니다. portMAX_DELAY로 설정하면 데이터가 쓰여질 때까지 무한정 기다립니다.
**/
esp_err_t i2s_write(i2s_port_t i2s_num, const void *src, size_t size, size_t *bytes_written, TickType_t ticks_to_wait);
정현파 오디오 데이터는 사인(sine) 함수를 사용하여 생성됩니다. 이는 특정 주파수와 진폭을 가진 부드러운 파형으로, 가장 기본적인 소리 신호입니다. I2S를 통해 이 정현파를 재생하면 '삐-'하는 순수한 소리를 들을 수 있어, 오디오 하드웨어 테스트에 유용합니다.
정현파 오디오 데이터 생성 코드를 다시 살펴보겠습니다.
#define SAMPLE_RATE 44100
#define SAMPLES_PER_CYCLE 128
#define SINE_WAVE_AMPLITUDE 20000
// 정현파 데이터 버퍼
int16_t sine_wave[SAMPLES_PER_CYCLE];
void create_sine_wave() {
for (int i = 0; i < SAMPLES_PER_CYCLE; i++) {
sine_wave[i] = (int16_t)(SINE_WAVE_AMPLITUDE * sin((2.0 * M_PI * i) / SAMPLES_PER_CYCLE));
}
}
SAMPLES_PER_CYCLE (주기당 샘플 수)는 128개의 샘플을 사용하여 정현파의 한 주기를 만듭니다. 다시 말해, (2.0 * M_PI * i) / SAMPLES_PER_CYCLE는 정현파의 한 주기를 0에서 2π 라디안까지 나누어주는 역할을 합니다.
그리고 계산된 사인 값에 SINE_WAVE_AMPLITUDE(20000)를 곱하여 파형의 크기를 조정합니다. 이 값은 16비트 정수 범위에 맞춰져 있습니다.
정리해보면 아래와 같은 데이터가 생성됩니다.
// 예시: 128 샘플로 한 주기 생성
sample[0] = 20000 × sin(0) = 0 // 0도
sample[32] = 20000 × sin(π/2) = 20000 // 90도
sample[64] = 20000 × sin(π) = 0 // 180도
sample[96] = 20000 × sin(3π/2) = -20000 // 270도
결과적으로, sine_wave 배열에는 128개의 샘플로 이루어진 정현파 데이터가 저장되며, 이 배열을 반복적으로 I2S로 전송하면 지정된 주파수의 소리가 계속해서 재생됩니다.
그렇다면 정현파 예제 코드에서 생성되는 소리의 주파수는 얼마일까요? 약 344.5Hz입니다. 샘플링 레이트가 44.1kHz 이므로 44100Hz ÷ 128샘플 = 약 344Hz의 기본 주파수를 가진 톤이 생성됩니다. 즉, 1초당 약 344번 반복되는 정현파가 생성된 거죠.

이 주파수는 일반적인 사람의 목소리 톤에 가까우며, 테스트용으로 듣기에 편안한 소리입니다.
만약에 1kHz 주파수를 가진 정현파를 생성하고 싶다면 어떻게 할까요? SAMPLES_PER_CYCLE을 44로 설정하면 됩니다. 44100Hz ÷ 1000Hz = 44.1 ≈ 44 샘플이 한 주기가 되어야 하기 때문입니다.
ESP32와 MAX98357A, 스피커를 연결하고 위와 같이 코드를 작성해서 적용시킨다음 스피커 동작을 확인했지만 소리가 나지 않았습니다.
혹시 핀을 잘못 연결했나 싶었지만 아니었고요... 코드가 잘못됐나 싶어서 다른 사람이 작성한 코드도 적용해봤지만 동작하지 않았습니다. 설마 스피커 문제인가 싶어서 스피커도 교체해봤지만 동작하지 않았고요.
그래서 오실로스코프를 통해 ESP32가 MAX98357A로 신호를 잘 보내는지 확인해봤습니다.
참고로... 프로브가 현재 1개이고, 채널은 2개라서... 1개 신호 밖에 측정을 못합니다. 프로브 하나 구매해서 2채널로 해서 비교 가능한데, 구매를 해야겠네요. ㅠㅠ
1. BCLK (클럭 신호)
BCLK(Bit Clock) 주파수는 ESP32의 I2S 설정에 따라 결정됩니다. MAX98357A는 클럭을 생성하는 마스터가 아니라 클럭을 제공받는 슬레이브이므로, BCLK의 주파수는 ESP32가 결정해요.

이 공식은 I2S 통신이 1초에 전송해야 하는 총 비트 수를 나타내기 때문에 '비트 전송률(Bit Rate)'이라고도 불립니다.

이 설정에서 BCLK 주파수는 1.4112 MHz가 됩니다. 이 주파수는 오디오 데이터가 얼마나 빠르게 전송되는지를 나타내며, MAX98357A는 이 클럭에 맞춰 데이터를 받아 소리로 변환합니다.
오실로스코프 측정 결과 1.41MHz로 측정되었습니다. 위 계산과 동일하네요.

2. WS/LRCK (워드 셀렉트)
WS/LRCK(워드 셀렉트) 주파수는 샘플링 속도와 동일한 44,100Hz입니다. WS는 좌/우 채널을 구분하는 신호로, 매 샘플마다 한 번씩 토글되므로 샘플링 속도와 같습니다.
WS = LOW: 왼쪽 채널 16비트 전송
WS = HIGH: 오른쪽 채널 16비트 전송
→ 1초에 44,100번 토글 = 44,100Hz
오실로스코프 측정 결과 약 44.09KHz. 위 결과와 역시 동일합니다.

3. DIN (데이터 입력)
오디오 데이터를 I2S로 전송할 때, 정현파를 생성해서 보내더라도 DIN(데이터) 핀에서 출력되는 신호의 모양은 사인파가 아니에요. DIN 핀에서는 0과 1로 이루어진 디지털 비트 스트림이 나옵니다.
ESP32의 DIN 출력은 PWM 신호가 아니라 PCM 디지털 데이터입니다. 정현파를 16비트 정수 배열로 변환한 디지털 샘플값들을 I2S 프로토콜에 따라 직렬로 전송하는 방식입니다. 즉, 각 샘플을 구성하는 16개의 비트가 BCLK(비트 클럭)에 맞춰 하나씩 차례대로 출력됩니다.
오실로스코프로 측정해보니 데이터 신호 역시 잘 나오는 것을 확인할 수 있습니다.

신호는 잘 보내는 거 같았고요. 근데 MAX98357A가 스피커로 내보내는 출력 신호는 잡히지 않았습니다. 음... 설마 했지만 MAX98357A 불량인건가!!!
다행히 MAX98357A가 비싸지 않아서 여분을 샀기 때문에 곧바로 교체해서 테스트해볼 수 있었습니다. 앗... 진짜 소리가 나네요. 출력 신호 역시 잘 잡히네요.

MAX98357A 불량이 맞았습니다. ㅠㅠ 역시 임베디드는 쉽지만은 않네요. 하드웨어 문제까지 확인해서 테스트해야 되니까요. 아무튼 좋은(?) 경험 하게되었습니다.
안녕하세요.
이 프로젝트는 회로 구성과 동작 원리를 명확하게 설명하여, 오디오 시스템을 배우고자 하는 메이커들에게 매우 유용한 자료입니다.
앞으로의 프로젝트를 후원하고 싶습니다. 혹시 관심이 있어요?