"Audio 실험실 #1: MAX98357A 오디오 증폭기 + 소형 스피커 소리 내기"를 이어서 어떤걸 해볼까 하다가 MicroSD 카드에다가 음원 파일을 저장해서 노래를 재생시켜보면 어떨까 생각해봤습니다. 과연 스피커 품질은 어느 정도일지 궁금해집니다.
마이크로 SD(MicroSD) 카드는 이동식 플래시 메모리 카드의 한 종류로, 현재 시판되는 메모리 카드 중 가장 작은 크기를 가지고 있습니다. 주로 스마트폰, 태블릿, 액션캠, 블랙박스 등 소형 모바일 장치의 저장 용량을 확장하는 용도로 사용되며, 용량 대비 가격 효율이 좋고 범용성이 매우 뛰어나 보급형 메모리 카드로 널리 사용됩니다. (크기는 일반적으로 15mm x 11mm x 1mm 정도입니다.)
마이크로 SD 카드는 지원하는 최대 저장 용량에 따라 다음과 같이 구분됩니다.
| 규격 | 전체 이름 | 최대 용량 | 파일 시스템 |
|---|---|---|---|
| SDSC(SD) | Secure Digital Standard Capacity | 최대 2GB | FAT16 |
| SDHC | Secure Digital High Capacity | 2GB 초과 ~ 32GB | FAT32 |
| SDXC | Secure Digital eXtended Capacity | 32GB 초과 ~ 2TB | exFAT |
마이크로 SD 카드의 속도는 최소 지속 쓰기 속도를 기준으로 여러 등급으로 나뉩니다. 용도에 따라 필요한 최소 속도를 보장하는 카드를 선택해야 합니다. 일반적으로 등급이 높을수록 고화질 영상 녹화나 고속 연속 사진 촬영에 적합합니다.
#1. 스피드 클래스 (Speed Class, C)
#2. UHS 스피드 클래스 (UHS Speed Class, U): UHS(Ultra-High Speed) 인터페이스를 사용하는 고속 등급입니다.
#3. 비디오 스피드 클래스 (Video Speed Class, V): 고해상도 비디오 및 VR 영상 촬영에 적합한 속도 등급입니다.
저는 쿠팡(앗...)에서 샌디스크 MicroSD 32GB를 구매한 적이 있습니다. 가격은 만원 정도합니다.

근데 확인해보시면 32GB 용량. HC는 SDHC를 의미하는 거 같고요. 10은 C10을 의미하네요. 잘 보면 10을 감싸고 있는게 원이 아니라 C인 느낌...
64GB, 128GB 제품을 살펴보면 XC라고 되어있는 제품을 보실 수 있을거에요. SDXC를 의미합니다.
상품 설명서에 비디오 속도는 C10, U1로 소개되어있는데 사실 같은 의미죠. 최소 10MB/s 속도인거고요. 읽기 속도는 최대 100MB/s 로 나와있어요.
🤔 C10, U1은 최소 10MB/s 속도인데, 읽기 속도가 왜 최대 100MB/s 라고 나와있는거죠?
C10 (Class 10)과 U1 (UHS-I Speed Class 1)은 최소 순차 쓰기 속도를 보장하는 등급입니다. 읽기 속도 최대 100MB/s는 최대 순차 읽기 속도를 나타냅니다. 이론상 최고 성능을 의미합니다.
즉, 플레시 메모리의 특성상 읽기가 쓰기보다 훨씬 빠릅니다.
실제로는 쓰기는 20-40MB/s 정도고요, 읽기는 80-90MB/s 정도의 속도가 나온다고 합니다.
결론적으로, 이 SD 카드는 비디오 녹화 시 최소 10MB/s로 안정적으로 쓸 수 있고, 컴퓨터로 파일을 복사할 때는 최대 100MB/s로 빠르게 읽을 수 있다는 의미입니다.
🤔 TF라는 말도 있는데, TF 카드는 뭐야?
TF 카드(TransFlash Card)는 마이크로 SD(MicroSD) 카드의 원래 이름입니다. 결론적으로, 현재 시장에서 말하는 TF 카드와 마이크로 SD 카드는 물리적으로나 기술적으로 거의 동일한 제품을 가리킨다고 보시면 됩니다.
2004년 SanDisk와 Motorola의 협력으로 모바일 기기용으로 출시된, 당시 세계에서 가장 작은 메모리 카드였습니다. 출시된 해 말, SD 협회(SDA)가 이 규격을 공식적으로 채택하면서 이름이 MicroSD 카드로 변경되었습니다.
마이크로 SD/TF 카드 메모리 실드 모듈 (Micro SD/TF Card Memory Shield Module)은 마이크로컨트롤러(예: 아두이노, 라즈베리 파이, AVR 등)와 같은 전자 장치에서 마이크로 SD 카드를 쉽고 안정적으로 사용할 수 있도록 만들어진 인터페이스 보드입니다.
구매처: https://ko.aliexpress.com/item/1005007211417449.html

이 모듈을 사용하면 프로젝트에 대용량 저장 기능을 추가하여 센서 데이터 기록, 이미지/음성 파일 저장, 설정값 저장 등 다양한 기능을 구현할 수 있습니다.
대부분의 마이크로 SD 카드 모듈은 다음 기술을 기반으로 작동합니다.
#1. 통신 방식: SPI (Serial Peripheral Interface)
#2. 전압 레벨 변환 (Level Shifting)
사용 시 유의사항은 다음과 같습니다.
가장 기본적인 실습부터 진행해보겠습니다. 마이크로 SD 카드 모듈을 ESP32의 ESP-IDF 환경에서 사용해봅니다. 이는 SD SPI Host Driver와 FATFS 라이브러리를 통해 표준 파일 시스템처럼 접근할 수 있어 효율적입니다.
Micro SD 카드 모듈은 일반적으로 SPI 통신을 사용하며, ESP32의 VSPI 버스에 연결하는 것이 일반적입니다.
| SD 카드 모듈 핀 | ESP32 GPIO (예시) | 설명 |
|---|---|---|
| VCC | 3.3V | 전원 (3.3V 또는 5V, 모듈에 따라 다름. 레벨시프터가 있다면 5V도 가능 |
| GND | GND | 접지 |
| CS (SS) | GPIO 5 | 칩 선택 (Chip Select) |
| MOSI | GPIO 23 | 마스터 출력, 슬레이브 입력 (데이터) |
| MISO | GPIO 19 | 마스터 입력, 슬레이브 출력 (데이터) |
| SCK(CLK) | GPIO 18 | 클럭 |
주의. SD 카드 모듈에 레벨 시프터가 내장되어 있지 않다면, ESP32의 3.3V 핀에만 연결해야 합니다. VCC를 5V에 연결하면 SD 카드가 손상될 수 있습니다.
저 같은 경우에는 TTGO T-Display ESP32 개발 보드를 사용하는데, 위 예시 핀 번호가 없는 경우가 있어서 CS 2번, MOSI 15, CLK 13, MISO 12 연결하도록 하겠습니다.
Micro SD 카드를 FAT32 파일 시스템으로 포맷합니다.
카드 루트 디렉터리 밑에 music이라는 폴더를 만들고 3개의 음원 파일(예: song1.mp3, track2.wav, music.ogg 등)을 저장합니다. 참고로, 저는 일본 노래를 좋아해서 파일 이름이 일본어 입니다. 형식은 mp3 파일입니다.
이 예시는 SD 카드를 /sdcard 마운트 지점으로 마운트하고, 해당 경로에서 파일 목록을 검색하여 음원 파일을 찾아 출력합니다.
#1. 헤더 파일 및 매크로 정의
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <dirent.h>
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#include "driver/sdspi_host.h"
#include "esp_log.h"
// 사용자 정의 SPI 핀 정의
#define PIN_NUM_MISO 21
#define PIN_NUM_MOSI 15
#define PIN_NUM_CLK 13
#define PIN_NUM_CS 2
// 마운트 포인트
#define MOUNT_POINT "/sdcard"
// 지원하는 음원 파일 확장자 목록
static const char* AUDIO_EXTENSIONS[] = {
".mp3", ".wav", ".ogg"
};
const int MAX_AUDIO_FILES= 5;
static const char *TAG = "MP3_PLAYER";
요약해보자면 SD 카드를 SPI로 읽고 FAT 파일시스템으로 마운트해서 파일/디렉토리를 탐색하는 데 필요한 헤더들입니다.
🤔 FAT? VFS? 용어가 낯선데 설명이 필요합니다...!
🤔 헤더 이름을 보면 sdmmc 라는 것도 있고, sdspi 라는 것도 있는데 무슨 의미인가요?
ESP32에서 SD 카드를 연결하는 방식은 크게 두 가지(SDSPI / SDMMC) 라고 보시면 됩니다. 그중에서 SDMMC는 “SD 카드 전용 고속 인터페이스”입니다.
SDMMC = SD Memory Card Controller 라는 의미로 ESP32 칩 안에 내장된 SD 카드용 전용 하드웨어 컨트롤러입니다. 즉, SD 카드를 가장 빠르고 안정적으로 쓰는 방식이라고 이해하시면 됩니다.
데이터 라인이 1비트(SPI)에서 4비트(SDMMC)로 늘어나서 더 많은 데이터를 한 번에 처리할 수 있게 됩니다. 근데 제가 사용하는 Micro SD/TF Card Memory Shield Module은 데이터 핀이 4개가 아닌데요? 네. 그러면 당연히 SDMMC 모드는 못 씁니다. 지금 가지고 계신 모듈은 무조건 SPI 방식 전용입니다.
💻 참고: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/storage/sdmmc.html

실제로 공식 문서에서도 esp_driver_sdmmc가 있고, esp_driver_sdspi 가 있는 것을 알 수 있습니다. "sdmmc_cmd.h"는 프로토콜 계층 드라이버이며, 호스트 드라이버는 SDMMC(sdmmc_host.h)와 SDSPI(sdspi_host.h) 드라이버로 나눠지는데, 둘 중 하나를 선택해서 사용하게 됩니다. 저는 sdspi_host.h 를 사용한 것이지요.
#2. SD 카드 초기화 (SPI 모드) 및 마운트 설정
// --- SD 카드 초기화 (SPI 모드) ---
esp_err_t sdcard_init(void)
{
// SPI 호스트 초기화
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
host.slot = SPI3_HOST; // ESP32에서 VSPI (SPI3) 사용
// SPI 버스 설정
spi_bus_config_t bus_cfg = {
.mosi_io_num = PIN_NUM_MOSI,
.miso_io_num = PIN_NUM_MISO,
.sclk_io_num = PIN_NUM_CLK,
.quadwp_io_num = -1, // 사용 안 함
.quadhd_io_num = -1, // 사용 안 함
.max_transfer_sz = 4000 // 최대 전송 크기
};
ESP_ERROR_CHECK(spi_bus_initialize(host.slot, &bus_cfg, SDSPI_DEFAULT_DMA));
// SPI 장치 설정
sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
slot_config.gpio_cs = PIN_NUM_CS;
slot_config.host_id = host.slot;
// FAT 파일 시스템 마운트 설정
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = false, // 마운트 실패 시 자동 포맷 안 함
.max_files = 5, // 동시에 열 수 있는 최대 파일 수
.allocation_unit_size = 16 * 1024 // 할당 단위
};
// SD 카드 마운트 (SPI 모드)
sdmmc_card_t *card;
esp_err_t ret = esp_vfs_fat_sdspi_mount(MOUNT_POINT, &host, &slot_config, &mount_config, &card);
if (ret != ESP_OK) {
ESP_LOGI(TAG, "Failed to mount SD card: %s", esp_err_to_name(ret));
return ret;
} else {
ESP_LOGI(TAG, "SD card mounted successfully!");
}
sdmmc_card_print_info(stdout, card); // 카드 정보 출력
return ret;
}
주의. sdspi_slot_config_t 대신에 sdspi_device_config_t 를 사용하세요. sdspi_slot_config_t는 deprecated 된 거 같습니다.
참고. SPI3_HOST vs SPI2_HOST 차이점
참고. max_files는 동시에 열 수 있는 파일의 개수(파일 핸들 개수) 제한을 의미합니다. 마치 도서관에서 책을 빌릴 수 있는 권수가 5권이라고 생각하면 됩니다. fopen()을 여러 번 호출해서 파일을 총 5개까지 열 수 있다는 의미입니다.
여기서 오해하지 말아야 될 게, 이 설정과 SD카드에 있는 파일 개수는 아무 상관 없습니다. 폴더 안에 파일이 100개 있어도 다 읽힐 수 있습니다. 단지 한 번에 열어둘 수 있는 파일 핸들 수만 5개라는 뜻이죠.
#3. 음원 파일 검사 함수
// --- 파일 이름이 음원 파일인지 확인하는 헬퍼 함수 ---
bool is_audio_file(const char *filename)
{
if (filename == NULL) return false;
// 파일 이름에서 마지막 .의 위치를 찾음
const char *dot = strrchr(filename, '.');
if (!dot || dot == filename) return false;
// 확장자를 소문자로 반환하여 비교
char ext[5];
strncpy(ext, dot, 4);
ext[4] = '\0';
for (int i = 0; i < strlen(ext); i++) {
ext[i] = tolower((unsigned char)ext[i]);
}
const int audio_ext_count = sizeof(AUDIO_EXTENSIONS) / sizeof(AUDIO_EXTENSIONS[0]);
for (int i = 0; i < audio_ext_count; i++) {
if (strcmp(ext, AUDIO_EXTENSIONS[i]) == 0) {
return true;
}
}
return false;
}
이 함수는 주어진 파일 이름의 확장자를 검사하여 미리 정의된 AUDIO_EXTENSIONS (예: .mp3, .wav) 중 하나와 일치하는지 확인합니다.
확장자를 소문자로 변환하여 대소문자 구분 없이 파일 형식을 확인할 수 있도록 합니다.
#4. 디렉터리 탐색 및 파일 목록 출력
// --- 특정 경로의 파일 목록 추출 ---
void sdcard_list_files(const char *path)
{
DIR *dir = opendir(path);
if (!dir) {
ESP_LOGI(TAG, "Failed to open directory: %s", path);
return;
//goto unmount;
}
ESP_LOGI(TAG, "Listing files in: %s", path);
struct dirent *entry;
int found_count = 0;
while ((entry = readdir(dir)) != NULL && found_count < MAX_AUDIO_FILES) {
// 파일 또는 폴더 이름
const char *filename = entry->d_name;
// "." 또는 ".." 디렉토리는 건너뜀
if (strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0) {
continue;
}
// 파일인지 확인하고 음원 파일인지 검사
if (entry->d_type == DT_REG && is_audio_file(filename)) {
printf("File: %s\n", filename);
found_count += 1;
}
// printf("Entry: %s (type: %d)\n", filename, entry->d_type);
}
closedir(dir);
if (found_count == 0) {
printf("음원 파일을 찾지 못했습니다.\n");
}
}
즉, 해당 디렉터리 경로의 음원 파일 여부를 확인해서 출력하는 코드입니다.
#5. 메인 함수
void app_main(void)
{
if (sdcard_init() != ESP_OK) {
ESP_LOGI(TAG, "SD card init failed");
return;
}
// music 폴더 파일 출력
sdcard_list_files("/sdcard/music");
// unmout:
// ESP_LOGI(TAG, "-----------------------");
// esp_vfs_fat_sdcard_unmount(MOUNT_POINT, card);
}
SD 카드 초기화 (SPI 모드) 및 마운트 설정을 진행한 후, music 폴더의 음원 파일 목록을 출력하도록 하는 코드입니다.
코드 작성이 끝나서 빌드하고 플래시(업로드)하려고 하니까 아래와 같이 에러가 나면서 진행되지 않습니다. 무슨 일일까요?
WARNING: Failed to communicate with the flash chip, read/write operations will fail.
Try checking the chip connections or removing any other hardware connected to IOs.
....
A fatal error occurred: Packet content transfer stopped (received 8 bytes)
ESP32 플래싱 과정에서 플래시 칩과 통신이 끊겼다는 의미고요. 플래싱 중에 PC ↔ ESP32 통신이 중간에 끊겨서 esptool이 패킷을 못 받는 상태입니다.
원인으로는 다양한게 있을 수 있습니다. USB 케이블 불안정, Baudrate 너무 높음, 보드 전류 부족, 보드 불량 등등... 저는 근데 이전에 잘 사용했던 보드라서 이 문제는 아니라고 봤습니다.
그러다가 특정 GPIO는 사용하면 문제가 발생할 수 있다는 글을 보게 되었습니다.
💻 참고: Failed to communicate with the flash chip - ESP32 Forum
2번과 12번 핀을 확인하라는 군요. 12번 핀은 VDD_SPI 도메인의 전압을 결정합니다. 2번 핀(0번 핀과 함께)은 부팅 모드을 결정합니다. 저는 2번과 12번 둘 다 사용하고 있었는데, 12번 핀을 다른 핀으로 바꿔서 다시 실행했더니 문제가 해결되었습니다.
🤔 근데 이럴거면 12번 핀은 아예 사용 못하도록 막아놔야되는거 아닌가요...?
GPIO12는 부팅 시 Flash 전압(3.3V/1.8V)을 결정하는 Strapping Pin입니다. 왜 막아놓지 않았냐면요. 실제로는 사용 가능합니다. 다만 조건부로... 아래와 같이 사용하면 괜찮다고 하네요.
// 1. 출력 핀으로 사용 (부팅 후 설정) - 괜찮음
gpio_set_direction(GPIO_NUM_12, GPIO_MODE_OUTPUT);
// 2. Pull-down 저항이나 floating 상태 - 괜찮음
gpio_set_pull_mode(GPIO_NUM_12, GPIO_PULLDOWN_ONLY);
문제가 되는 경우는 아래와 같다고 합니다.
// Pull-up 저항 연결 - 부팅 실패!
gpio_set_pull_mode(GPIO_NUM_12, GPIO_PULLUP_ONLY);
// 부팅 시 HIGH 신호를 주는 장치 연결 - 부팅 실패!
이처럼 조심해야될 필요가 있는 핀이 있다는 사실~ 0번, 2번, 12번 정도... 네, 잘 알게되었습니다.
저는 MicroSD 카드에다가 music 폴더를 만들고 3개의 mp3 파일을 저장했습니다. 그리고 코드에서 마운트 포인트를 '/sdcard/music'로 했습니다. 그리고 opendir를 동일한 경로로 설정했습니다.
그리고 나서 확인해보니 파일은 없고 MUSIC 이란 폴더가 하나 더 있네요? 그래서 opendir를 '/sdcard/music/MUSIC'으로 했더니 그제서야 안에 mp3 파일이 제대로 나왔습니다. 왜 그런걸까요?
이건 마운트 포인트 개념을 잘못 이해했기 때문입니다. 마운트 포인트는 SD 카드의 루트(/)를 ESP32의 어느 경로에 연결할지 정하는 것입니다. SD 카드 내부의 특정 폴더를 지정하는 게 아닙니다.
따라서 그냥 SD 루트를 /sdcard에 마운트 하고요. opendir할 때 실제 폴더 경로인 "/sdcard/music" 으로 접근하면 되겠습니다.
일본어 파일명이 깨지는 건 FAT 파일시스템의 LFN(Long File Name) 인코딩 설정 문제입니다. FATFS + ESP-IDF가 기본적으로 ANSI(OEM) 코드페이지만 사용하기 때문인데요. 즉, SD카드 안의 파일이 일본어(UTF-8)로 저장되어 있으면 FATFS가 읽어올 때 깨져 보이는 게 정상입니다.
Entry: ��뫷~1.MP3 (type: 1)
Entry: TUKI-~1.MP3 (type: 1)
Entry: ��뫷~2.MP3 (type: 1)
해결 방법으로는 menuconfig에서 설정하거나 sdkconfig 파일에 직접 추가해주면 됩니다. 저는 menuconfig 를 열어서 "Component config → FAT Filesystem support → Long filename support" 을 수정하도록 하겠습니다.
보시면 Enable LFN (long filename support)가 기본적으로 "No long filenames" 라고 되어있습니다. 이거를 "Long filename buffer in heap" 으로 바꿔줍니다.
일본어/한글/UTF-8 파일명을 다루려면 파일 이름이 길 수 있기 때문에 스택보다 힙이 안전합니다. 스택은 태스크별로 크기가 작기 때문에 오버플로 가능성이 있습니다.
그리고 나서 API character enconding 을 "API uses UTF-8 enconding" 으로 바꿔줍니다. FATFS 내부 UTF-16 파일명을 UTF-8로 변환해서 리턴해줍니다.

그리고 나서 다시 빌드하고 플래시해서 결과를 확인해보면~ 네 잘 나오네요.
File: ヨルシカ - 晴る.mp3
File: tuki - 晩餐歌.mp3
File: ヨルシカ - ただ君に晴れ.mp3
오늘은 아쉽지만 여기까지 하고요. 다음 시간에는 "MAX98357A 오디오 증폭기 + 소형 스피커" 에다가 SD 카드를 연동 시켜서 노래를 재생 시켜보도록 하겠습니다.
최종적으로는 OLED 디스플레이까지 연동시켜서 간단한 MP3 플레이어 비슷하게 만들어보면 어떨까 생각합니다.
또 다른 예시로는 SD 카드말고, Wi-Fi나 Bluetooth를 이용해서 스마트폰으로 실시간 스트리밍도 해보면 좋을거라 생각합니다. 재밌는 예시가 많겠군요.