这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » RISC-V » 合宙ESP32C3+PCM5102模块播放WAV文件

共1条 1/1 1 跳转至

合宙ESP32C3+PCM5102模块播放WAV文件

专家
2026-06-08 18:15:45     打赏

之前使用合宙ESP32C3驱动MAX98357播放SD卡中WAV文件。在程序不做任何修改的情况下,将MAX98357换做PCM5102,依旧可以正常播放wav文件。电路连接:

图片1.png

播放对象是之前Max98357使用的那四个wav文件。程序代码:

#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);  
}

不过和Max98357相比,播放的音量明显小了一些。

微信图片_20260608181315_7_2.jpg





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

共1条 1/1 1 跳转至

回复

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