迷你小板子尺寸适配的esp32c3 super mini 开发板。(但这个板子射频电路不好,也是要慎用。)

音频小板子,用的NS4168音频i2s音频解码功放IC(资料:http://www.nsiway.com.cn/product/18.html)和MSM261S4030H0R数字麦克风(资料:https://item.szlcsc.com/datasheet/MSM261S4030H0R/3020642.html

原理图非常简单,单纯就是根据IC资料拉线就完事了。

所以板子背面可以直接焊接esp32c3super mini,也可以通过杜邦线连接其他开发板。

根据如图与其他开发连接即可。

NS4168使用时需要注意一个问题,这个IC有个问题,在停止发送i2s音频数据后,芯片不会自动将喇叭输出的电压归零,有时会导致喇叭发热到烫手,所以在初始化后和音频播放结束后,一定要播放一段空白音频,让喇叭发出最后一个一个音时空白音频。

下面时一个基本录音和放音的范例,按住boot(IO9)开始录音,松开boot播放录音。esp32c3只有一路i2S音频,所以在录放两个设备切换前要重新初始化i2s。

#include "FS.h"
#include "SPIFFS.h"
#define FORMAT_SPIFFS_IF_FAILED true

#include <driver/i2s.h>
#define REC_I2S_WS 7
#define REC_I2S_SD 5
#define REC_I2S_SCK 6
//#define I2S_LR_RX 3

#define I2S_PORT_0 I2S_NUM_0
#define SAMPLE_RATE 16000
#define BUTTON_PIN 9

#define PLAY_I2S_LRC 3
#define PLAY_I2S_BCLK 2
#define PLAY_I2S_DIN 1


#define BUFSIZE 128
int16_t audioData[BUFSIZE]; //i2s缓存
int16_t zeroData[128]; //空白音频
uint recordingSize = 0;
bool wste = 0;

void setup() {
  pinMode(BUTTON_PIN, INPUT);
  for(int i = 0; i < 128; i ++){
    zeroData[i] = 0x00;
  }

  Serial.begin(115200);
  if (!SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)) {   //初始化SPIFFS,存储音频数据
    Serial.println("SPIFFS Mount Failed");
    return;
  }
  //SPIFFS.format();
  listDir(SPIFFS, "/", 0);
}

void loop() {
  if (digitalRead(BUTTON_PIN) == LOW) {
    delay(20);
    if (digitalRead(BUTTON_PIN) == LOW) {
      i2s_rx_init();   //初始化录音I2S模式
      Serial.println("Recording...");
      size_t bytes_read = 0;
      recordingSize = 0;
      File file = SPIFFS.open("/pcm", FILE_WRITE);
      while (digitalRead(BUTTON_PIN) == LOW) {
        esp_err_t result = i2s_read(I2S_PORT_0, audioData, sizeof(audioData), &bytes_read, portMAX_DELAY);
        for (int i = 0; i < BUFSIZE; i++) {
          int16_t data = audioData[i];
          byte* bytes = (byte*)&data;
          file.write(bytes, sizeof(int16_t));
        }
        recordingSize += bytes_read / 2;
      }
      file.close();
      Serial.print(recordingSize);
      Serial.println(" Recording complete.");
      i2s_driver_uninstall(I2S_PORT_0);
      wste = 1;
    }
  }

  if (wste) {
    i2s_tx_init();  //初始化播放I2S模式

    Serial.println("Playing...");
    size_t bytes_written = 0;
    File file = SPIFFS.open("/pcm", FILE_READ);
    if (!file) {
      Serial.println("无法打开文件");
      return;
    }
    while (file.available()) {
      int bytesRead = file.readBytes((char*)audioData, sizeof(audioData));

      if (bytesRead > 0) {
        amplifyAudio(audioData,BUFSIZE,5);  //放大音量
        esp_err_t result = i2s_write(I2S_PORT_0, audioData, bytesRead, &bytes_written, portMAX_DELAY);
      } else {
        // End of file reached, close the file
        file.close();
        Serial.println("File closed");
      }
    }
    esp_err_t result = i2s_write(I2S_PORT_0, zeroData, 16, &bytes_written, portMAX_DELAY);
    Serial.println("Playing complete.");
    i2s_zero_dma_buffer(I2S_PORT_0);  // 清空I2S DMA缓冲区

    i2s_driver_uninstall(I2S_PORT_0);

    wste = 0;
  }
}

void amplifyAudio(int16_t* aData, int dataSize, float gain) {
  for (int i = 0; i < dataSize; i++) {
    aData[i] = aData[i] * gain;
  }
}

void i2s_rx_init() {
  const i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = i2s_bits_per_sample_t(16),
    .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
    .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S),
    .intr_alloc_flags = 0,  // default interrupt priority
    .dma_buf_count = 8,
    .dma_buf_len = 512,
    .use_apll = false
  };

  esp_err_t err = i2s_driver_install(I2S_PORT_0, &i2s_config, 0, NULL);
  if (err != ESP_OK) {
    Serial.printf("I2S driver install failed (I2S_PORT_0): %d\n", err);
    while (true)
      ;
  }

  const i2s_pin_config_t pin_config = {
    .bck_io_num = REC_I2S_SCK,
    .ws_io_num = REC_I2S_WS,
    .data_out_num = I2S_PIN_NO_CHANGE,
    .data_in_num = REC_I2S_SD
  };
  err = i2s_set_pin(I2S_PORT_0, &pin_config);
  if (err != ESP_OK) {
    Serial.printf("I2S set pin failed (I2S_PORT_0): %d\n", err);
    while (true)
      ;
  }
}

void i2s_tx_init() {
  const i2s_config_t i2sOut_config = {
    .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_TX),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = i2s_bits_per_sample_t(16),
    .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
    .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S),
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 8,
    .dma_buf_len = BUFSIZE
  };
  esp_err_t err = i2s_driver_install(I2S_PORT_0, &i2sOut_config, 0, NULL);
  if (err != ESP_OK) {
    Serial.printf("I2S driver install failed (I2S_PORT_0): %d\n", err);
    while (true)
      ;
  }

  const i2s_pin_config_t i2sOut_pin_config = {
    .bck_io_num = PLAY_I2S_BCLK,
    .ws_io_num = PLAY_I2S_LRC,
    .data_out_num = PLAY_I2S_DIN,
    .data_in_num = -1
  };
  err = i2s_set_pin(I2S_PORT_0, &i2sOut_pin_config);
  if (err != ESP_OK) {
    Serial.printf("I2S set pin failed (I2S_PORT_1): %d\n", err);
    while (true)
      ;
  }
}

void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
    Serial.printf("Listing directory: %s\r\n", dirname);

    File root = fs.open(dirname);
    if(!root){
        Serial.println("- failed to open directory");
        return;
    }
    if(!root.isDirectory()){
        Serial.println(" - not a directory");
        return;
    }

    File file = root.openNextFile();
    while(file){
        if(file.isDirectory()){
            Serial.print("  DIR : ");
            Serial.println(file.name());
            if(levels){
                listDir(fs, file.path(), levels -1);
            }
        } else {
            Serial.print("  FILE: ");
            Serial.print(file.name());
            Serial.print("\tSIZE: ");
            Serial.println(file.size());
        }
        file = root.openNextFile();
    }
}