这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » MCU » SD读写模块与Max98357模块的联动测试(二)-----测试MAX98357

共2条 1/1 1 跳转至

SD读写模块与Max98357模块的联动测试(二)-----测试MAX98357模块

专家
2026-06-03 13:13:41     打赏

使用树莓派RP2040测试MAX98357时,RP2040本身没有I2S,用的是软件模拟方式,效果不理想,表现为播放的音频速度明显变慢。最终使用ESP32C3成功。ESP32C3有一个I2S外设可以使用这个外设完成播放。

在测试之前,我准备了四个用于测试的WAV文件,音频的整体时间长度不完全相同。

t1.wav:16位、双通道、采样率44100Hz。

t2.wav:16位、双通道、采样率48000Hz。

t3.wav:16位、单通道、采样率48000Hz。

t4.wav:16位、双通道、采样率48000Hz。

把这四个wav文件拷贝到sd卡中。然后将sd卡取出,插入到SD卡读写模块里。将SD卡读写模块连接到ESP32C3。同时,将Max98357也连接到ESP32C3中。

 

图片4.png


实物:

图片3.png

程序代码如下:

#include <SPI.h>
#include <SD.h>
#include <driver/i2s.h>
 
// ========== SD卡引脚 (避开GPIO8/9,防止下载冲突) ==========
#define SD_CS      10
#define SD_MOSI    6
#define SD_MISO    7
#define SD_SCK     4
 
// ========== MAX98357A I2S引脚 ==========
#define I2S_BCK    5    // 位时钟 (BCLK)
#define I2S_WS     3    // 左右时钟 (LRC)
#define I2S_DOUT   2    // 数据输出 (DIN)
 
// ========== I2S配置 ==========
#define I2S_PORT           I2S_NUM_0
#define BUFFER_SIZE        512      // 音频缓冲区大小
 
// WAV文件头结构
struct wav_header_t {
    char chunkID[4];        // "RIFF"
    uint32_t chunkSize;
    char format[4];         // "WAVE"
    char subchunk1ID[4];    // "fmt "
    uint32_t subchunk1Size;
    uint16_t audioFormat;
    uint16_t numChannels;
    uint32_t sampleRate;
    uint32_t byteRate;
    uint16_t blockAlign;
    uint16_t bitsPerSample;
    char subchunk2ID[4];    // "data"
    uint32_t subchunk2Size;
};
 
wav_header_t wavHeader;
File currentFile;
 
// I2S引脚配置
i2s_pin_config_t i2s_pins = {
    .bck_io_num = I2S_BCK,
    .ws_io_num = I2S_WS,
    .data_out_num = I2S_DOUT,
    .data_in_num = I2S_PIN_NO_CHANGE
};
 
// I2S配置 - 标准模式
i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
    .sample_rate = 44100,
    .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 = 256,
    .use_apll = true,   // <--- 启用APLL,提高时钟精度
    .tx_desc_auto_clear = true,
    .fixed_mclk = -1
};
 
// ========== 函数声明 ==========
bool readWAVHeader(File& file, wav_header_t* header);
void printWAVInfo(wav_header_t* header);
void setI2SSampleRate(uint32_t sampleRate);
bool playWAVFile(const char* filename);
 
// ========== 初始化I2S (带GPIO驱动强度修复) ==========
void initI2S() {
    Serial.println("正在初始化I2S...");
    
    // 安装I2S驱动
    esp_err_t err = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
    if (err != ESP_OK) {
        Serial.printf("I2S驱动安装失败: %d\n", err);
        return;
    }
    
    // 设置I2S引脚
    err = i2s_set_pin(I2S_PORT, &i2s_pins);
    if (err != ESP_OK) {
        Serial.printf("I2S引脚设置失败: %d\n", err);
        return;
    }
    
    gpio_set_drive_capability((gpio_num_t)I2S_DOUT, GPIO_DRIVE_CAP_0);
    
    Serial.println("I2S初始化完成 (DOUT驱动能力已降为最低)");
}
 
// ========== 初始化SD卡 ==========
bool initSDCard() {
    Serial.println("正在初始化SD卡...");
    
    // 初始化自定义SPI总线
    SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
    
    if (!SD.begin(SD_CS, SPI)) {
        Serial.println("SD卡初始化失败!");
        return false;
    }
    
    Serial.println("SD卡初始化成功");
    
    // 打印SD卡信息
    uint64_t cardSize = SD.cardSize() / (1024 * 1024);
    Serial.printf("SD卡容量: %llu MB\n", cardSize);
    
    return true;
}
 
// ========== 读取WAV文件头 ==========
bool readWAVHeader(File& file, wav_header_t* header) {
    if (file.read((uint8_t*)header, sizeof(wav_header_t)) != sizeof(wav_header_t)) {
        Serial.println("读取WAV头失败");
        return false;
    }
    
    // 验证WAV格式
    if (memcmp(header->chunkID, "RIFF", 4) != 0 ||
        memcmp(header->format, "WAVE", 4) != 0 ||
        memcmp(header->subchunk1ID, "fmt ", 4) != 0) {
        Serial.println("不是有效的WAV文件");
        return false;
    }
    
    return true;
}
 
// ========== 打印WAV文件信息 ==========
void printWAVInfo(wav_header_t* header) {
    Serial.println("========== WAV文件信息 ==========");
    Serial.printf("采样率: %lu Hz\n", header->sampleRate);
    Serial.printf("声道数: %d\n", header->numChannels);
    Serial.printf("位深: %d bit\n", header->bitsPerSample);
    Serial.printf("数据大小: %lu bytes\n", header->subchunk2Size);
    Serial.println("=================================");
}
 
// ========== 动态调整I2S采样率 ==========
void setI2SSampleRate(uint32_t sampleRate) {
    // 停止I2S
    i2s_stop(I2S_PORT);
    
    // 更新配置(采样率降低一半,否则播放太快,变调;双声道变单声道,减半?)
    i2s_config.sample_rate = sampleRate/2;
    
    // 重新配置
    i2s_driver_uninstall(I2S_PORT);
    i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
    i2s_set_pin(I2S_PORT, &i2s_pins);
    
    // 重新应用驱动强度修复
    gpio_set_drive_capability((gpio_num_t)I2S_DOUT, GPIO_DRIVE_CAP_0);
    
    Serial.printf("I2S采样率已调整为: %lu Hz\n", sampleRate/2);
}
 
// ========== 播放WAV文件 ==========
bool playWAVFile(const char* filename) {
    Serial.printf("正在播放: %s\n", filename);
    
    // 打开文件
    File file = SD.open(filename);
    if (!file) {
        Serial.println("无法打开文件");
        return false;
    }
    
    // 读取并验证WAV头
    if (!readWAVHeader(file, &wavHeader)) {
        file.close();
        return false;
    }
    
    printWAVInfo(&wavHeader);
    
    // 根据WAV文件的采样率调整I2S
    setI2SSampleRate(wavHeader.sampleRate);
    
    // 准备缓冲区
    uint8_t* buffer = (uint8_t*)malloc(BUFFER_SIZE);
    if (!buffer) {
        Serial.println("内存分配失败");
        file.close();
        return false;
    }
    
    // ⚠️ 立体声转单声道处理
    // MAX98357A是单声道功放,立体声数据需要转换[citation:1]
    bool isStereo = (wavHeader.numChannels == 2);
    size_t bytesRemaining = wavHeader.subchunk2Size;
    size_t bytesToRead;
    int16_t* sampleBuffer;
    int16_t* monoSamples;
    
    Serial.println("开始播放...");
    
    while (bytesRemaining > 0) {
        bytesToRead = (bytesRemaining < BUFFER_SIZE) ? bytesRemaining : BUFFER_SIZE;
        
        // 读取音频数据
        size_t bytesRead = file.read(buffer, bytesToRead);
        if (bytesRead == 0) break;
        
        if (isStereo) {
            // 立体声转单声道:只取左声道(每4字节为一对立体声16位样本)
            size_t sampleCount = bytesRead / 4;
            monoSamples = (int16_t*)malloc(sampleCount * sizeof(int16_t));
            if (monoSamples) {
                sampleBuffer = (int16_t*)buffer;
                for (size_t i = 0; i < sampleCount; i++) {
                    monoSamples[i] = sampleBuffer[i * 2];  // 只取左声道
                }
                // 发送转换后的单声道数据
                i2s_write(I2S_PORT, monoSamples, sampleCount * sizeof(int16_t), &bytesRead, portMAX_DELAY);
                free(monoSamples);
            } else {
                // 内存不足时发送原始数据(可能产生噪音)
                i2s_write(I2S_PORT, buffer, bytesRead, &bytesRead, portMAX_DELAY);
            }
        } else {
            // 单声道直接发送
            i2s_write(I2S_PORT, buffer, bytesRead, &bytesRead, portMAX_DELAY);
        }
        
        bytesRemaining -= bytesToRead;
        
        // 简单的进度显示
        static uint32_t lastPrint = 0;
        if (millis() - lastPrint > 1000) {
            int progress = (int)((wavHeader.subchunk2Size - bytesRemaining) * 100.0 / wavHeader.subchunk2Size);
            Serial.printf("播放进度: %d%%\n", progress);
            lastPrint = millis();
        }
    }
    
    free(buffer);
    file.close();
    
    Serial.println("播放完成!");
    return true;
}
 
// ========== 列出SD卡中的WAV文件 ==========
void listWAVFiles() {
    Serial.println("\nSD卡中的WAV文件:");
    
    File root = SD.open("/");
    if (!root) {
        Serial.println("无法打开根目录");
        return;
    }
    
    File file = root.openNextFile();
    int count = 0;
    
    while (file) {
        if (!file.isDirectory()) {
            String name = file.name();
            if (name.endsWith(".wav") || name.endsWith(".WAV")) {
                Serial.printf("  - %s (%llu bytes)\n", name.c_str(), file.size());
                count++;
            }
        }
        file = root.openNextFile();
    }
    
    if (count == 0) {
        Serial.println("  没有找到WAV文件");
    }
    
    root.close();
}
 
// ========== Arduino标准函数 ==========
void setup() {
    Serial.begin(115200);
    delay(1000);
    
    Serial.println("\n==================================");
    Serial.println("ESP32-C3 SD卡 WAV播放器");
    Serial.println("==================================\n");
    
    // 初始化SD卡
    if (!initSDCard()) {
        Serial.println("SD卡初始化失败,系统停止");
        while (1) delay(100);
    }
    
    // 列出所有WAV文件
    listWAVFiles();
    
    // 初始化I2S音频
    initI2S();
    
    // 设置音量(通过修改数据幅度)
    // MAX98357A没有软件音量控制,可以在播放时调整buffer数据
    
    // 查找并播放第一个WAV文件
    File root = SD.open("/");
    File file = root.openNextFile();
    bool found = false;
    
    //while (file && !found) {
    while (file) {
        if (!file.isDirectory()) {
            String name = file.name();
            if (name.endsWith(".wav") || name.endsWith(".WAV")) {
                playWAVFile((String(F("/")) + name).c_str());
                //found = true;
            } else {
                //found = false;
            }
        }
        file = root.openNextFile();
    }
    root.close();
    
    if (!found) {
        Serial.println("未找到任何WAV文件!");
    }
}
 
void loop() {
    // 播放完成后空闲
    delay(1000);
}


注意在处理采样率那一块(函数:setI2SSampleRate):

代码:i2s_config.sample_rate = sampleRate/2;

进行了人为干涉,将采样率降低了一半。不这样做的结果,播放声音会音调变高。

以下是测试过程中的输出日志:

 
==================================
ESP32-C3 SD卡 WAV播放器
==================================
 
正在初始化SD卡...
SD卡初始化成功
SD卡容量: 59 MB
 
SD卡中的WAV文件:
  - t2.wav (6381996054500764 bytes)
  - t3.wav (3191093051595824 bytes)
  - t1.wav (23053684708038044 bytes)
  - t4.wav (6381996054486064 bytes)
正在初始化I2S...
I2S初始化完成 (DOUT驱动能力已降为最低)
正在播放: /t2.wav
========== WAV文件信息 ==========
采样率: 48000 Hz
声道数: 2
位深: 16 bit
数据大小: 1485880 bytes
=================================
I2S采样率已调整为: 24000 Hz
开始播放...
播放进度: 0%
播放进度: 12%
播放进度: 25%
播放进度: 38%
播放进度: 51%
播放进度: 64%
播放进度: 77%
播放进度: 90%
播放完成!
正在播放: /t3.wav
========== WAV文件信息 ==========
采样率: 48000 Hz
声道数: 1
位深: 16 bit
数据大小: 742940 bytes
=================================
I2S采样率已调整为: 24000 Hz
开始播放...
播放进度: 3%
播放进度: 15%
播放进度: 29%
播放进度: 41%
播放进度: 54%
播放进度: 67%
播放进度: 80%
播放进度: 93%
播放完成!
正在播放: /t1.wav
========== WAV文件信息 ==========
采样率: 44100 Hz
声道数: 2
位深: 16 bit
数据大小: 5367560 bytes
=================================
I2S采样率已调整为: 22050 Hz
开始播放...
播放进度: 1%
播放进度: 4%
播放进度: 8%
播放进度: 11%
播放进度: 14%
播放进度: 18%
播放进度: 21%
播放进度: 24%
播放进度: 28%
播放进度: 31%
播放进度: 34%
播放进度: 38%
播放进度: 41%
播放进度: 44%
播放进度: 47%
播放进度: 51%
播放进度: 54%
播放进度: 57%
播放进度: 61%
播放进度: 64%
播放进度: 67%
播放进度: 71%
播放进度: 74%
播放进度: 77%
播放进度: 81%
播放进度: 84%
播放进度: 87%
播放进度: 91%
播放进度: 94%
播放进度: 97%
播放完成!
正在播放: /t4.wav
========== WAV文件信息 ==========
采样率: 48000 Hz
声道数: 2
位深: 16 bit
数据大小: 1485880 bytes
=================================
I2S采样率已调整为: 24000 Hz
开始播放...
播放进度: 3%
播放进度: 16%
播放进度: 29%
播放进度: 42%
播放进度: 55%
播放进度: 68%
播放进度: 81%
播放进度: 94%
播放完成!
未找到任何WAV文件!


 测试结果(音频部分对比):

图片5.png

按照制作时的采样率播放,音调变高了。从对比图上看,按我的理解,相当于按照高频率的模式播放,所以音调变高了。而减半后播放,声音才变得正常。





关键词: 大懒猫的试用笔记     ESP32C3     Max98357    

专家
2026-06-03 13:50:54     打赏
2楼

这个变调的问题,我不知道和Max98357是单声道模块有没有关系。后面准备用双声道的I2S模块、在不改变采样率的条件下播放试试。


共2条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]