ESP32: запись звука с INMP441 на SD-карту по сигналу VOS

     Для тех, кто заходил сюда до 09.10.2026, сообщаю об ошибке в скетче.

     Как ни странно, даже при длительном тестировании я не заметил, что дата и время записи файлов на SD-карте не меняются. Просто не обращал на это внимания — смотрел только в монитор последовательного порта, где всё было ОК.

     Скетч ESP32_VOS_INMP441_SD_Test_02.ino исправлен, как и листинг на странице.

     Добавлю каплю мёда. Вам не нужно каждый раз зажимать кнопку BOOT (P0) и нажимать RST (EN), чтобы загрузить скетч. Схема платы спроектирована так, что чип CH343 сам управляет линиями P0 и EN через сигналы DTR и RTS, поступающие от компьютера.

 

Эта статья является развитием следующего проекта для ESP32:

https://conntest.ru/program/zapis-zvuka-s-inmp441-na-sd-kartu-s-pomoshhyu-esp32

Алгоритм работы:

— ESP32 выводит в монитор версию программы, дату и время по запросу с сервера, параметры SD карты, передает на Arduino UNO размер свободного пространства SD.
— Запись звука начинается по сигналу от датчика звука VOS (Vibration Oscillation Sensor) на микросхеме LM393. После прекращения сигналов от датчика запись продолжается еще 10 секунд. Если в течение этого времени датчик снова срабатывает, отсчет 10 секунд начинается заново.
— После того, как датчик звука перестает подавать сигналы в течение 10 секунд, происходит запись на SD.
— Светодиод (LED) горит постоянно, когда идет процесс записи, не горит в ожидании звука, мигает в случае ошибки SD или при достижении максимального номера файла (можно назначать произвольное значение).

     Предлагаемая реализация не является оптимальным рабочим скетчем, так как во время разработки было введено много тестовых сообщений и взаимодействие с UNO, но служит хорошим наглядным примером и может работать без UNO. Хотя, по-моему, мало что изменится, если убрать вывод тестовых сообщений. Важно, тестовые сообщения, например, “File ready: open=1551256 us, header=5190 us, total=1556641 us” помогут оценить качество карты и провести сравнение. При примерно одинаковом количестве файлов разные SD-карты дают различие по времени открытия чуть ли не на порядок.

     С устойчивостью к продолжительной записи, по результату тестирования, скетч справляется, но замечена чувствительность к некачественным картам. Тестирование проводилось с картами на 4, 16, 32 и 48 ГБ. 4-х гигабайтная оказалась сбойной, хотя с компьютером более-менее работает. Насчет 48-гигабайтной можно почитать здесь:

https://conntest.ru/program/prevrashhaem-fejkovyj-ssd-na-4-tb-v-realnuyu-sdxc-na-48-gb

     Установлено, что с возрастанием количества записанных файлов на карту увеличивается время “SD OPEN”. Перенос файлов в каталог не устраняет проблему, только немного смягчает. Замена #include «FS.h» и #include «SD.h» на #include «SdFat.h» сократила время процентов на 30. Параметр #define SD_SPI_SPEED SD_SCK_MHZ(16) (16 МГц) сработал с картой на 16 ГБ, на 20 МГц – карта ушла в отказ. Свой окончательный вариант для автономной работы выложу, когда соберу устройство на аккумуляторе, а вы сейчас можете проанализировать временные характеристики по логам и модернизировать скетч согласно вашим предпочтениям.

Примененный датчик звука:

https://conntest.ru/program/modul-datchika-zvuka-vos-arduino-lm398-digitalserial

     Хочу отметить, что применение 4-контактного голосового модуля датчика обнаружения звука мне не понравилось.

https://conntest.ru/program/4-kontaktnyj-modul-datchika-obnaruzheniya-zvuka-arduino

     Сначала я реализовал включение записи по опросу, но затем перешел на обработку по прерыванию. По ходу тестирования выявилось много проблем, видимо, не связанных с методом регистрации звука, поэтому сейчас я не уверен, что режим опроса хуже режима по прерыванию, но проверять уже не буду.

     Таблица подключения модулей к ESP32-WROOM (в Arduino IDE выбрал ESP32 Dev Module, обозначения контактов ESP32 относятся к плате расширения):

КНОПКИSDESP32INMP441VOSUART_UNOLED_PINS
RSTEN
BOOTP0
GNDGND
VCC5V
MISOP19
MOSIP23
SCKP18
CS(SS)P5
P14WS
P32SCK
P33SD
GNDL/R
3,3VVDD
GNDGND
3,3VVCC
GNDGND
P4OUT
P26UNO_TX
P25UNO_RX
P2KATHODE
3,3V4,7 k -> ANODE
Плата расширения ESP32
Плата расширения памяти Micro SD
Микрофон INMP441

    Из-за того, что долго не получалось создать надежную реализацию, пришлось автоматизировать процесс тестирования. Для этого я применил Arduino UNO со звуковым модулем ISD1820 и мини-колонками. Из таблицы видно к каким контактам ESP32 подключается UNO. Сначала подключение было таким: RXD2 — P16, TXD2 — P17 с делителем на RXD2. Через пару недель выход TXD2 деградировал, на UNO висело +5В. Применил модуль оптопары на 6N137 с AliExpress (как будто остальное не оттуда же). Со стороны ESP32: VCC – 3,3V, UNO_RX – RX1, UNO_TX – TX1. Со стороны UNO: VCC – 5V, RX – RX2, TX – TX2.

Модуль оптопары на 6N137

    Скажу честно, что для тестирования скетча связь ESP32 с UNO и дисплей теперь мне кажутся излишними. Хотя, не мешают и придают динамичности. 

Скетч для ESP32:

// ============================================================
// ESP32 Audio Recorder v7.3 - SdFat
// ДИАГНОСТИКА SD OPEN / PREBUFFER
// ============================================================

#include <Arduino.h>
#include <driver/i2s.h>

#include "SdFat.h"
#include "SPI.h"

#include <WiFi.h>
#include "time.h"

// ============================================================
// Wi-Fi
// ============================================================

const char* ssid     = "Имя сети";
const char* password = "Пароль";

// ============================================================
// Часовой пояс
// ============================================================

// Смещение относительно UTC в часах.
// Устанавливается вручную.

#define TIMEZONE_OFFSET_HOURS 8

// ============================================================
// UART UNO
// ============================================================

#define UNO_TX 26
#define UNO_RX 25

HardwareSerial UnoSerial(2);

// ============================================================
// SD
// ============================================================

#define SD_CS 5
#define SD_SPI_SPEED SD_SCK_MHZ(16)

SdFs sd;
FsFile audioFile;

uint64_t cardSize = 0;
uint64_t freeSpaceMB = 0;

// ============================================================
// I2S / INMP441
// ============================================================

#define I2S_WS     14
#define I2S_SD     33
#define I2S_SCK    32
#define I2S_PORT   I2S_NUM_0

#define SAMPLE_RATE 16000
#define BUFFER_LEN 1024

// ============================================================
// LED / датчик
// ============================================================

#define LED_PIN          2
#define SOUND_SENSOR_PIN 4

// ============================================================
// Аудио
// ============================================================

#define PREBUFFER_SECONDS 1 // Длительность звукового фрагмента из памяти, сохраняемого перед моментом обнаружения звука.
#define RINGBUFFER_SECONDS 1  // Объём кольцевого буфера в RAM в секундах; в нём постоянно хранится самый свежий звук.

// Количество отсчётов для pre-buffer; рассчитывается из частоты дискретизации и его длительности.
#define PREBUFFER_SAMPLES (SAMPLE_RATE * PREBUFFER_SECONDS)

// Количество отсчётов кольцевого буфера; определяет, сколько секунд звука одновременно хранится в RAM.
#define RINGBUFFER_SAMPLES (SAMPLE_RATE * RINGBUFFER_SECONDS)

// ============================================================
// WAV
// ============================================================

#define WAV_HEADER_SIZE 44

// ============================================================
// Тишина
// ============================================================

// Время тишины в миллисекундах, после которого текущая запись автоматически останавливается.
const unsigned long SILENCE_TIMEOUT = 10000;  

// ============================================================
// I2S буферы
// ============================================================

int32_t i2sBuffer[BUFFER_LEN];
int16_t convertedBuffer[BUFFER_LEN];

// ============================================================
// RAM ring buffer
// ============================================================

// Кольцевой буфер в RAM для хранения последних отсчётов и формирования pre-buffer.
int16_t ringBuffer[RINGBUFFER_SAMPLES]; 

// Индекс позиции, куда будет записан следующий отсчёт в кольцевом буфере.
volatile uint32_t ringWriteIndex = 0; 

// Средство защиты ring buffer при одновременном обращении из разных задач.
portMUX_TYPE ringMux = portMUX_INITIALIZER_UNLOCKED;

// ============================================================
// Sound interrupt
// ============================================================

volatile bool soundDetect = false;

// Средство защиты флага обнаружения звука при работе обработчика прерывания.
portMUX_TYPE soundMux = portMUX_INITIALIZER_UNLOCKED;

// ============================================================
// Recording state
// ============================================================

volatile bool isRecording = false;
volatile bool startingRecording = false;
volatile bool recorderReady = false;
volatile bool prebufferSaved = false;

bool sdError = false;
bool sdCriticalError = false;

uint32_t bytesWrittenTotal = 0; // Общее количество байтов аудиоданных, записанных в текущий WAV-файл.
uint32_t recordStartIndex = 0;  // Позиция в ring buffer, соответствующая моменту начала текущей записи.

int16_t NextFilename = 0;
int16_t MaxFilename = 9999;
char filename[32];

unsigned long lastSoundDetectionTime = 0; // Время последнего обнаружения звука; используется для отсчёта периода тишины.
unsigned long lastDebugTime = 0;  // Время последнего диагностического сообщения о состоянии таймера тишины.
unsigned long lastFlushTime = 0;  // Время последнего принудительного сброса буфера данных файла на SD.
unsigned long sdErrorLedTime = 0; // Время последнего изменения состояния светодиода при ошибке SD.
bool sdErrorLedState = false; // Текущее состояние светодиода, используемое для индикации ошибки SD.
unsigned long maxWriteTime = 0; // Максимальное зафиксированное время одной операции записи в SD.
uint32_t sdWriteErrors = 0; // Счётчик ошибок записи аудиоданных на SD.

// ============================================================
// Синхронизация SD write / stopRecording
// ============================================================

// Показывает, выполняется ли в данный момент запись данных в SD из I2S-задачи.
volatile bool sdWriteInProgress = false;  

// Защищает флаг записи SD и состояние записи от одновременного доступа.
portMUX_TYPE sdWriteMux = portMUX_INITIALIZER_UNLOCKED; 

// ============================================================
// Фильтр
// ============================================================

float alpha = 0.95;
float hpFilterState = 0.0;
float hpFilterPrevInput = 0.0;


// ============================================================
// FreeRTOS
// ============================================================

TaskHandle_t i2sTaskHandle = NULL;  // Дескриптор задачи FreeRTOS, которая непрерывно получает звук от I2S.

// ============================================================
// Прототипы
// ============================================================

float highPassFilter(float input);

void writeWavHeader(
  FsFile &file,
  int sampleRate,
  int bitsPerSample,
  int channels,
  uint32_t dataSize
);

void i2s_install();
void i2s_setpin();

void generateFilenameBinary();
bool prepareNextFile();
bool startRecordingFromBuffer();
void stopRecording();

void handleSoundSensor();

void sendFreeSpaceToUNO(uint64_t freeMB);
String waitForUNOResponse(unsigned long timeoutMs);

void syncTimeIfPossible();
void printTimeIfAvailable();

void i2sCaptureTask(void* parameter);
void savePrebufferToFile();
void handleSDError();

// ============================================================
// Время
// ============================================================

void syncTimeIfPossible() {

  Serial.print("Wi-Fi sync... ");

  WiFi.begin(ssid, password);

  unsigned long startAttempt = millis();

  while (
    WiFi.status() != WL_CONNECTED &&
    millis() - startAttempt < 8000
  ) {
    delay(500);
    Serial.print(".");
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.println(" OK");

    configTime(
      TIMEZONE_OFFSET_HOURS * 3600,
      0,
      "pool.ntp.org",
      "time.nist.gov"
    );

    struct tm timeinfo;
    int retry = 0;

    while (!getLocalTime(&timeinfo) && retry < 10) {
      delay(500);
      retry++;
    }

    if (retry < 10) {
      Serial.print("Time: ");
      Serial.println(&timeinfo, "%Y-%m-%d %H:%M:%S");
    }

  } else {
    Serial.println(" skipped");
  }

  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
}

void printTimeIfAvailable() {
  struct tm timeinfo;

  if (getLocalTime(&timeinfo)) {

    Serial.printf(
      "%04d-%02d-%02d %02d:%02d:%02d",
      timeinfo.tm_year + 1900,
      timeinfo.tm_mon + 1,
      timeinfo.tm_mday,
      timeinfo.tm_hour,
      timeinfo.tm_min,
      timeinfo.tm_sec
    );

  } else {

    Serial.print("no time");
  }
}

// ============================================================
// Поиск первого свободного номера
//
// Этот поиск выполняется ТОЛЬКО ОДИН РАЗ при старте.
// После этого используется исключительно NextFilename++.
// ============================================================

void generateFilenameBinary() {

  int low = 0;
  int high = 9999;

  Serial.println("\n========== FILENAME SEARCH ==========");

  while (low <= high) {

    int mid = low + (high - low) / 2;

    sprintf(filename, "/record_%04d.wav", mid);

    unsigned long t0 = micros();
    bool exists = sd.exists(filename);
    unsigned long dt = micros() - t0;

    Serial.printf(
      "exists(%s) = %s, time=%lu us",
      filename, exists ? "YES" : "NO", dt
    );

    uint8_t errorCode = sd.sdErrorCode();
    uint8_t errorData = sd.sdErrorData();

    if (errorCode || errorData) {
      Serial.printf(
        "  SdFat errorCode=0x%02X, errorData=0x%02X",
        errorCode, errorData
      );
    }

    Serial.println();

    if (exists)
      low = mid + 1;
    else
      high = mid - 1;
  }

  NextFilename = low;

  sprintf(filename, "/record_%04d.wav", NextFilename);

  Serial.printf("First free file: %s\n", filename);
  Serial.println("====================================");
}

// ============================================================
// Создание следующего WAV
// ============================================================

bool prepareNextFile() {

  sprintf(
    filename, "/record_%04d.wav", NextFilename);

  Serial.print("Preparing file: ");
  Serial.println(filename);

  unsigned long t0 = micros();

  // ==========================================================
  // ДИАГНОСТИКА SdFat open()
  // ==========================================================

  unsigned long openStart = micros();

  audioFile.open(filename, O_WRITE | O_CREAT | O_AT_END);

  unsigned long openTime = micros() - openStart;

  // Результат открытия файла и время, затраченное на операцию open().
  Serial.printf("SD OPEN RESULT: valid=%s, time=%lu us\n", audioFile ? "YES" : "NO", openTime);

  if (!audioFile) {

    Serial.println("ERROR: cannot open WAV");

    // Код и дополнительные данные последней ошибки SdFat.
    Serial.printf("SdFat errorCode=0x%02X, errorData=0x%02X\n", sd.sdErrorCode(), sd.sdErrorData());

    sdError = true;

    return false;
  }

  // Текущая позиция указателя файла сразу после его открытия.
  Serial.printf("File position immediately after open: %u\n", (unsigned int)audioFile.position());

  // Размер файла, который SdFat видит сразу после открытия.
  Serial.printf("File size immediately after open: %u\n", (unsigned int)audioFile.size());

  // ==========================================================
  // ДИАГНОСТИКА WAV HEADER
  // ==========================================================

  unsigned long headerStart = micros();

  writeWavHeader(
    audioFile,
    SAMPLE_RATE,
    16,
    1,
    0
  );

  unsigned long headerTime = micros() - headerStart;

  // Время записи WAV-заголовка и состояние файла после этой операции.
  Serial.printf(
    "WAV HEADER RESULT: time=%lu us, position=%u, size=%u\n",
    headerTime,
    (unsigned int)audioFile.position(),
    (unsigned int)audioFile.size()
  );

  audioFile.seek(
    WAV_HEADER_SIZE
  );

  unsigned long totalTime = micros() - t0;

  // Время открытия файла, записи заголовка и общее время подготовки нового WAV.
  Serial.printf(
    "File ready: open=%lu us, header=%lu us, total=%lu us\n",
    openTime,
    headerTime,
    totalTime
  );

  recorderReady = true;
  prebufferSaved = false;
  bytesWrittenTotal = 0;
  lastFlushTime = millis();

  return true;
}

// ============================================================
// WAV HEADER
// ============================================================

void writeWavHeader(
  FsFile &file,
  int sampleRate,
  int bitsPerSample,
  int channels,
  uint32_t dataSize
  ) {

  file.seek(0);

  file.write((const uint8_t*)"RIFF", 4);

  uint32_t chunkSize = 36 + dataSize;

  file.write((byte*)&chunkSize, 4);

  file.write((const uint8_t*)"WAVE", 4);

  file.write((const uint8_t*)"fmt ", 4);

  uint32_t subchunk1Size = 16;
  uint16_t audioFormat = 1;
  uint16_t numChannels = channels;
  uint32_t sampleRate32 = sampleRate;
  uint16_t bitsPerSample16 = bitsPerSample;

  uint16_t blockAlign = channels * bitsPerSample /8;

  uint32_t byteRate = sampleRate * channels * bitsPerSample / 8;

  file.write((byte*)&subchunk1Size, 4);

  file.write((byte*)&audioFormat, 2);

  file.write((byte*)&numChannels, 2);

  file.write((byte*)&sampleRate32, 4);

  file.write((byte*)&byteRate, 4);

  file.write((byte*)&blockAlign, 2);

  file.write((byte*)&bitsPerSample16, 2);

  file.write((const uint8_t*)"data", 4);

  file.write((byte*)&dataSize, 4
  );
}

// ============================================================
// I2S
// ============================================================

void i2s_install() {

  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_32BIT,

    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,

    .communication_format = I2S_COMM_FORMAT_STAND_I2S,

    .intr_alloc_flags = 0,

    .dma_buf_count = 8,

    .dma_buf_len = BUFFER_LEN,

    .use_apll = false,

    .tx_desc_auto_clear = false,

    .fixed_mclk = 0
  };

  esp_err_t err =
    i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);

  if (err != ESP_OK) {

    Serial.printf("I2S install error: %d\n", err);

    sdCriticalError = true;
  }
}

void i2s_setpin() {

  const i2s_pin_config_t pin_config = {

    .bck_io_num = I2S_SCK,

    .ws_io_num = I2S_WS,

    .data_out_num = I2S_PIN_NO_CHANGE,

    .data_in_num = I2S_SD
  };

  i2s_set_pin(I2S_PORT, &pin_config);
}

// ============================================================
// Фильтр
// ============================================================

float highPassFilter(float input) {

  float output = alpha * (hpFilterState + input - hpFilterPrevInput);

  hpFilterState = output;

  hpFilterPrevInput = input;

  return output;
}

// ============================================================
// ISR
// ============================================================

void IRAM_ATTR handleInterrupt() {

  portENTER_CRITICAL_ISR(&soundMux);

  soundDetect = true;

  portEXIT_CRITICAL_ISR(&soundMux);
}

// ============================================================
// Сохранение pre-buffer из ring buffer
// ============================================================

void savePrebufferToFile() {

  if (prebufferSaved) return;

  if (!audioFile) return;

  unsigned long t0 = millis();

  static int16_t prebuffer[PREBUFFER_SAMPLES];

  portENTER_CRITICAL( &ringMux);

  uint32_t startIndex = recordStartIndex;

  if (startIndex >= PREBUFFER_SAMPLES) {

    startIndex -= PREBUFFER_SAMPLES;

  } else {

    startIndex = RINGBUFFER_SAMPLES - (PREBUFFER_SAMPLES - startIndex);
  }

  for (uint32_t i = 0; i < PREBUFFER_SAMPLES; i++) {

    prebuffer[i] = ringBuffer[startIndex];

    startIndex++;

    if (startIndex >= RINGBUFFER_SAMPLES) {
      startIndex = 0;
    }
  }

  portEXIT_CRITICAL(&ringMux);

  audioFile.seek(WAV_HEADER_SIZE);

  const uint8_t* data = (const uint8_t*)prebuffer;
  
  size_t bytesTotal = PREBUFFER_SAMPLES * sizeof(int16_t);

  size_t offset = 0;

  // ==========================================================
  // ДИАГНОСТИКА PRE-BUFFER
  // ==========================================================

  Serial.println();
  Serial.println("========== PREBUFFER DIAGNOSTICS ==========");

  // Объём данных, который должен быть записан в файл из pre-buffer.
  Serial.printf("PREBUFFER requested: %u bytes\n", (unsigned int)bytesTotal);

  // Позиция в WAV-файле непосредственно перед началом записи pre-buffer.
  Serial.printf("File position before write: %u\n", (unsigned int)audioFile.position());

  // Размер WAV-файла непосредственно перед записью pre-buffer.
  Serial.printf("File size before write: %u\n", (unsigned int)audioFile.size());

  // ==========================================================
  // Запись кусками по 1024 байта
  // ==========================================================

  while (offset < bytesTotal) {

    size_t remaining = bytesTotal - offset;

    size_t chunk = remaining > 1024 ? 1024 : remaining;

    unsigned long writeStart = micros();

    size_t written = audioFile.write(data + offset, chunk);

    unsigned long writeTime = micros() - writeStart;

    // Показывает положение в pre-buffer, размер запрошенной и фактически записанной части и время записи.
    Serial.printf(
      "PREBUFFER WRITE: offset=%u, requested=%u, wrote=%u, time=%lu us\n",
      (unsigned int)offset,
      (unsigned int)chunk,
      (unsigned int)written,
      writeTime
    );

    if (written != chunk) {

      Serial.println("PREBUFFER WRITE ERROR");

      // Подробно показывает, на каком месте pre-buffer произошла неполная запись.
      Serial.printf(
        "PREBUFFER ERROR DETAILS: offset=%u, requested=%u, wrote=%u\n",
        (unsigned int)offset,
        (unsigned int)chunk,
        (unsigned int)written
      );

      // Позиция указателя файла сразу после обнаружения ошибки записи.
      Serial.printf(
        "File position after ERROR: %u\n", (unsigned int)audioFile.position());

      // Размер файла, который SdFat видит после ошибки записи.
      Serial.printf("File size after ERROR: %u\n", (unsigned int)audioFile.size());

      // Общее время, прошедшее с начала сохранения pre-buffer до ошибки.
      Serial.printf("PREBUFFER elapsed: %lu ms\n", millis() - t0);

      Serial.println("==========================================");

      sdError = true;

      return;
    }

    offset += written;

    bytesWrittenTotal += written;

    // ========================================================
    // ДИАГНОСТИКА FLUSH ПОСЛЕ КАЖДОГО PRE-BUFFER WRITE
    // ========================================================

    unsigned long flushStart = micros();

    audioFile.flush();

    unsigned long flushTime = micros() - flushStart;

    // Показывает положение после записи очередного блока и время принудительной записи буфера файла на SD.
    Serial.printf("PREBUFFER FLUSH: offset=%u, time=%lu us\n", (unsigned int)offset, flushTime);
  }

  prebufferSaved = true;

  unsigned long dt = millis() - t0;

  // Итог: сколько байтов записано, сколько ожидалось, и сколько времени заняло сохранение pre-buffer.
  Serial.printf("PREBUFFER RESULT: wrote=%u expected=%u, time=%lu ms\n", (unsigned int)offset, (unsigned int)bytesTotal, dt);

  // Позиция указателя файла после полного сохранения pre-buffer.
  Serial.printf("File position after pre-buffer: %u\n", (unsigned int)audioFile.position());

  // Размер файла после полного сохранения pre-buffer.
  Serial.printf("File size after pre-buffer: %u\n", (unsigned int)audioFile.size());

  Serial.println("==========================================");
}

// ============================================================
// I2S CAPTURE TASK
// ============================================================

void i2sCaptureTask(void* parameter) {

  size_t bytesRead;

  while (true) {

    esp_err_t result =
      i2s_read(I2S_PORT, (void*)i2sBuffer, sizeof(i2sBuffer), &bytesRead, pdMS_TO_TICKS(100));

    if (result != ESP_OK || bytesRead == 0) {
      continue;
    }

    int samples = bytesRead / sizeof(int32_t);

    // ========================================================
    // Обработка аудио
    // ========================================================

    for (int i = 0; i < samples; i++) {

      int32_t sample24 = i2sBuffer[i] >> 9;

      float filteredSample = highPassFilter(sample24);

      float amplified = constrain(filteredSample * 2.0f, -32768.0f, 32767.0f);

      convertedBuffer[i] = (int16_t)amplified;
    }

    // ========================================================
    // Запись в ring buffer
    // ========================================================

    portENTER_CRITICAL(&ringMux);

    for (int i = 0; i < samples; i++) {

      ringBuffer[ringWriteIndex] = convertedBuffer[i];

      ringWriteIndex++;

      if (ringWriteIndex >= RINGBUFFER_SAMPLES) {

        ringWriteIndex = 0;
      }
    }

    portEXIT_CRITICAL(&ringMux);

    // ========================================================
    // Если идёт запись - пишем в SD
    // ========================================================

    bool doSDWrite = false;

    // --------------------------------------------------------
    // Атомарно решаем, может ли I2S task начать SD write.
    // --------------------------------------------------------

    portENTER_CRITICAL(&sdWriteMux);

    if (isRecording && audioFile) {

      sdWriteInProgress = true;

      doSDWrite = true;
    }

    portEXIT_CRITICAL(&sdWriteMux);

    if (!doSDWrite) {
      continue;
    }

    // ========================================================
    // Pre-buffer
    // ========================================================

    if (!prebufferSaved) {

      savePrebufferToFile();

      if (sdError) {

        portENTER_CRITICAL(&sdWriteMux);

        sdWriteInProgress = false;

        portEXIT_CRITICAL(&sdWriteMux);

        isRecording = false;

        continue;
      }
    }

    // ========================================================
    // Обычная запись
    // ========================================================

    size_t bytesToWrite = samples * sizeof(int16_t);

    unsigned long writeStart = micros();

    size_t bytesWritten = audioFile.write((byte*)convertedBuffer, bytesToWrite);

    // ========================================================
    // Повторы только при wrote=0
    // ========================================================

    int writeAttempt = 1;

    while (bytesWritten == 0 && writeAttempt < 3) {

      // Сообщает о повторной попытке записи после случая, когда SD не приняла ни одного байта.
      Serial.printf("SD WRITE RETRY #%d: wrote=0, waiting 10 ms\n", writeAttempt);

      delay(10);

      unsigned long retryStart = micros();

      bytesWritten = audioFile.write((byte*)convertedBuffer, bytesToWrite);

      unsigned long retryTime = micros() - retryStart;

      // Результат повторной попытки: сколько байтов записано и сколько времени она заняла.
      Serial.printf(
        "SD WRITE RETRY #%d RESULT: wrote=%u expected=%u, time=%lu us\n",
        writeAttempt,
        (unsigned int)bytesWritten,
        (unsigned int)bytesToWrite,
        retryTime
      );

      writeAttempt++;
    }

    unsigned long writeTime = micros() - writeStart;

    // ========================================================
    // Максимальное время записи
    // ========================================================

    if (writeTime > maxWriteTime) {

      maxWriteTime = writeTime;

      // Новое максимальное время одной операции записи в SD, зафиксированное за весь прогон.
      Serial.printf("SD WRITE MAX: %lu us\n", maxWriteTime);
    }

    // ========================================================
    // Медленная запись
    // ========================================================

    if (writeTime > 20000) {

      // Сообщение о записи, которая заняла более 20 мс; позволяет обнаруживать заметные задержки SD.
      Serial.printf("SD WRITE SLOW: %lu us, bytes=%u\n", writeTime, (unsigned int)bytesToWrite);
    }

    // ========================================================
    // Ошибка записи
    // ========================================================

    if (bytesWritten != bytesToWrite) {

      sdWriteErrors++;

      // Номер ошибки и результат записи: сколько байтов записано вместо требуемого количества.
      Serial.printf(
        "SD WRITE ERROR #%lu: wrote=%u expected=%u, time=%lu us\n",
        (unsigned long)sdWriteErrors,
        (unsigned int)bytesWritten,
        (unsigned int)bytesToWrite,
        writeTime
      );

      sdError = true;
      isRecording = false;

    } else {

      bytesWrittenTotal += bytesWritten;
    }

    // ========================================================
    // Периодический flush
    // ========================================================

    if (millis() - lastFlushTime > 60000) {

      audioFile.flush();

      lastFlushTime = millis();
    }

    // ========================================================
    // Сообщаем stopRecording(),
    // что SD write завершён
    // ========================================================

    portENTER_CRITICAL(&sdWriteMux);

    sdWriteInProgress = false;

    portEXIT_CRITICAL(&sdWriteMux);
  }
}

// ============================================================
// START RECORDING - МГНОВЕННЫЙ СТАРТ
// ============================================================

bool startRecordingFromBuffer() {

  if (startingRecording || isRecording) {
    return false;
  }

  if (sdCriticalError || sdError) {
    return false;
  }

  if (!recorderReady || !audioFile) {

    Serial.println("ERROR: WAV not prepared");

    return false;
  }

  startingRecording = true;

  hpFilterState = 0.0;
  hpFilterPrevInput = 0.0;

  portENTER_CRITICAL(&ringMux);

  recordStartIndex = ringWriteIndex;

  portEXIT_CRITICAL(&ringMux);

  portENTER_CRITICAL(&sdWriteMux);

  isRecording = true;

  portEXIT_CRITICAL(&sdWriteMux);

  prebufferSaved = false;
  bytesWrittenTotal = 0;
  lastFlushTime = millis();

  digitalWrite(LED_PIN, LOW);

  Serial.println("Recording started (with pre-buffer)");

  startingRecording = false;

  return true;
}

// ============================================================
// STOP RECORDING
// ============================================================

void stopRecording() {

  if (!isRecording) {
    return;
  }

  digitalWrite(LED_PIN, HIGH);

  // ==========================================================
  // Запрещаем I2S task начинать новый SD write.
  // Если write уже идёт — он продолжится до конца.
  // ==========================================================

  portENTER_CRITICAL(&sdWriteMux);

  isRecording = false;

  bool writeInProgress = sdWriteInProgress;

  portEXIT_CRITICAL(&sdWriteMux);

  // ==========================================================
  // Ждём завершения текущего write/retry
  // ==========================================================

  if (writeInProgress) {

    Serial.println("Waiting for SD write to finish...");

    unsigned long waitStart = millis();

    while (sdWriteInProgress) {

      delay(1);

      if (millis() - waitStart > 5000) {

        Serial.println("WARNING: SD write wait timeout");

        break;
      }
    }

    // Время, которое stopRecording() ожидал завершения текущей операции записи в SD.
    Serial.printf("SD write finished, wait=%lu ms\n", millis() - waitStart);
  }

  // Общее количество аудиоданных, накопленных в текущем WAV к моменту остановки.
  Serial.printf("Stopping recording, data=%lu bytes\n", (unsigned long)bytesWrittenTotal);

  // ==========================================================
  // Ждём пока pre-buffer сохранится
  // ==========================================================

  int waitCount = 0;

  while (!prebufferSaved && waitCount < 100) {

    delay(10);
    waitCount++;
  }

  if (sdError) {

    handleSDError();

    return;
  }

  // ==========================================================
  // Обновляем WAV header
  // ==========================================================

  writeWavHeader(
    audioFile,
    SAMPLE_RATE,
    16,
    1,
    bytesWrittenTotal
  );

  // ==========================================================
  // Закрываем файл
  // ==========================================================

  unsigned long closeStart = micros();

  audioFile.close();

  unsigned long closeTime = micros() - closeStart;

  // Время, затраченное на закрытие текущего WAV-файла.
  Serial.printf("File close: %lu us\n", closeTime);

  recorderReady = false;

  Serial.print("Saved: ");

  // Имя только что сохранённого WAV-файла.
  Serial.printf("/record_%04d.wav [", NextFilename);

  printTimeIfAvailable();

  Serial.println("]");

  // ==========================================================
  // никакого поиска следующего имени.
  // ==========================================================

  if (NextFilename < MaxFilename) {
      NextFilename++;
    } else {
    Serial.println("10000 файлов записаны, рекомендую прослушать");
    sdCriticalError = true;
    return;
    }

  // ==========================================================
  // Подготавливаем следующий файл
  // ==========================================================

  if (!prepareNextFile()) {

    Serial.println("ERROR preparing next file");

    sdError = true;
  }
}

// ============================================================
// SOUND SENSOR
// ============================================================

void handleSoundSensor() {

  bool localSoundDetect = false;

  portENTER_CRITICAL(&soundMux);

  if (soundDetect) {

    localSoundDetect = true;

    soundDetect = false;
  }

  portEXIT_CRITICAL(&soundMux);

  // Новый звук
  if (localSoundDetect) {

    lastSoundDetectionTime = millis();

    if (!isRecording && !sdCriticalError && !sdError) {

      Serial.println("Sound detected");

      startRecordingFromBuffer();
    }
  }

  // Контроль тишины
  if (isRecording) {

    unsigned long now = millis();

    unsigned long silence = now - lastSoundDetectionTime;

    if (silence >= SILENCE_TIMEOUT) {

      Serial.println("10 seconds silence - STOP");

      stopRecording();

    } else {

      if (now - lastDebugTime > 1000) {

        unsigned long remaining = SILENCE_TIMEOUT -silence;

        // Сколько секунд осталось до автоматической остановки записи при отсутствии нового импульса от датчика звука.
        Serial.printf("Silence timeout: %lu sec\n", remaining / 1000);

        lastDebugTime = now;
      }
    }
  }
}

// ============================================================
// UART
// ============================================================

void sendFreeSpaceToUNO(
  uint64_t freeMB
) {

  UnoSerial.printf("FREE=%llu\n", freeMB
  );

  UnoSerial.flush();
}

String waitForUNOResponse(unsigned long timeoutMs) {

  unsigned long startTime = millis();

  String response = "";

  while (millis() - startTime < timeoutMs) {

    if (UnoSerial.available()) {

      char c =UnoSerial.read();

      if (c == '\n') {

        response.trim();

        if (response.indexOf("OK") >= 0) {

          return "OK";
        }

        return response;

      } else if (c != '\r') {

        response += c;
      }
    }

    delay(1);
  }

  return "";
}

// ============================================================
// SD ERROR
// ============================================================

void handleSDError() {

  Serial.println("SD error");

  sdError = true;

  if (audioFile) {
    audioFile.close();
  }

  recorderReady = false;
  isRecording = false;
  startingRecording = false;
}

// Колбэк для библиотеки SdFat

void dateTimeCallback(uint16_t* date, uint16_t* time, uint8_t* ms10) {
  struct tm timeinfo;
  
  if (getLocalTime(&timeinfo)) {
    // Возвращаем дату с помощью макроса FS_DATE (Год, Месяц, День)
    *date = FS_DATE(timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday);
    // Возвращаем время с помощью макроса FS_TIME (Часы, Минуты, Секунды)
    *time = FS_TIME(timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
    // Точность в миллисекундах (опционально, для FAT систем)
    *ms10 = timeinfo.tm_sec % 2 * 100; 
  } else {
    // Дефолтное время, если синхронизация ещё не прошла
    *date = FS_DATE(2026, 10, 9);
    *time = FS_TIME(3, 0, 0);
    *ms10 = 0;
  }
}


// ============================================================
// SETUP
// ============================================================

void setup() {

  Serial.begin(115200);

  pinMode(LED_PIN, OUTPUT);

  digitalWrite(LED_PIN, HIGH);

  delay(500);

  Serial.println(
    "\nESP32 Audio Recorder v7.3 - SdFat"
  );

  Serial.println(
    "I2S + RAM Ring Buffer + Instant Start"
  );

  Serial.println(
    "ESP32_VOS_INMP441_SD_Test_02.ino"
  );

  // ==========================================================
  // RTC
  // ==========================================================

  syncTimeIfPossible();

  // ==========================================================
  // UART
  // ==========================================================

  UnoSerial.begin(9600, SERIAL_8N1, UNO_RX, UNO_TX);

  delay(1000);

  // ==========================================================
  // SD / SdFat
  // ==========================================================

  // ==========================================================
// SD / SdFat
// ==========================================================

Serial.println("SD power-up stabilization delay: 1000 ms");

// Даём карте и линии SPI полностью стабилизироваться после
// включения питания или перезапуска ESP32.
//
// Особенно важно при коротком reset ESP32, когда сама SD-карта
// могла не потерять питание и остаться в промежуточном состоянии.
delay(1000);

Serial.println("Initializing SD with SdFat...");

if (!sd.begin(SD_CS, SD_SPI_SPEED)) {

  Serial.println("SD error!");

  sd.initErrorPrint();

  sdCriticalError = true;

  return;
}

  Serial.println("SdFat SD initialized OK");

  // Регистрация времени для FsFile
  FsDateTime::setCallback(dateTimeCallback);

  Serial.println("SD post-init stabilization delay: 500 ms");

  // Карта уже успешно ответила на инициализацию.
  // Даём ей немного времени перед первыми файловыми операциями
  // и обращением к FAT.
  //
  // Это важно проверить именно потому, что в проблемном случае
  // sd.begin() проходит, но позднее sd.exists() ведёт себя так,
  // будто файловая система пуста.
  delay(500);

  // ==========================================================
  // Тип карты
  // ==========================================================

  Serial.print("SD: ");

  switch (sd.card()->type()) {

    case SD_CARD_TYPE_SD1:

      Serial.println("SD1");

      break;

    case SD_CARD_TYPE_SD2:

      Serial.println("SD2");

      break;

    case SD_CARD_TYPE_SDHC:

      Serial.println("SDHC / SDXC");

      break;

    default:

      Serial.println("UNKNOWN");

      break;
  }

  // ==========================================================
  // Размер карты
  // sectorCount() = количество 512-байтовых секторов
  // ==========================================================

  uint32_t sectorCount = sd.card()->sectorCount();

  cardSize = ((uint64_t)sectorCount * 512ULL) / (1024ULL * 1024ULL);

  Serial.printf("Size: %llu MB\n", cardSize);

  // ==========================================================
  // Свободное место
  //
  // В SdFat это считается через количество свободных кластеров.
  // ==========================================================

  int32_t freeClusters = sd.vol()->freeClusterCount();

  if (freeClusters >= 0) {

    uint64_t bytesPerCluster = sd.vol()->bytesPerCluster();

    freeSpaceMB =((uint64_t)freeClusters * bytesPerCluster) / (1024ULL * 1024ULL);

  } else {

    freeSpaceMB = 0;
  }

  Serial.printf("Free: %llu MB\n", freeSpaceMB
  );

  // ==========================================================
  // UNO
  // ==========================================================

  Serial.println("Wait UNO...");

  bool dataConfirmed = false;

  int attempts = 0;

  while (!dataConfirmed && attempts < 3) {

    sendFreeSpaceToUNO(freeSpaceMB);

    String response = waitForUNOResponse(3000);

    if (response == "OK") {

      dataConfirmed = true;

      Serial.println("UNO OK");
    }

    attempts++;

    delay(500);
  }

  if (!dataConfirmed) {

    Serial.println("No UNO, continue");
  }

  // ==========================================================
  // I2S
  // ==========================================================

  i2s_install();

  i2s_setpin();

  i2s_start(I2S_PORT);

  // ==========================================================
  // Filename
  //
  // Только первоначальный поиск.
  // ==========================================================

  Serial.println("Delay before first FAT file search: 500 ms");

// К этому моменту карта уже:
// 1. прошла sd.begin()
// 2. была опрошена по типу
// 3. читался sectorCount()
// 4. читалась информация о свободном месте.
//
// Даём дополнительную паузу непосредственно перед первой
// массовой файловой операцией sd.exists().
//
// Это диагностическая задержка для редкого случая, когда
// карта определяется нормально, но поиск существующих файлов
// внезапно возвращает false.
delay(500);

Serial.println("Starting first filename search...");

generateFilenameBinary();

  // ==========================================================
  // Prepare FIRST WAV
  // ==========================================================

  if (!prepareNextFile()) {

    sdCriticalError = true;

    return;
  }

  // ==========================================================
  // Sound input
  // ==========================================================

  pinMode(SOUND_SENSOR_PIN, INPUT);

  attachInterrupt(SOUND_SENSOR_PIN, handleInterrupt, FALLING);

  // ==========================================================
  // Start I2S capture task
  // ==========================================================

#if CONFIG_FREERTOS_UNICORE

  xTaskCreate(
    i2sCaptureTask,
    "I2S Capture",
    8192,
    NULL,
    3,
    &i2sTaskHandle
  );

#else

  xTaskCreatePinnedToCore(
    i2sCaptureTask,
    "I2S Capture",
    8192,
    NULL,
    3,
    &i2sTaskHandle,
    1
  );

#endif

  // ==========================================================
  // Ready
  // ==========================================================

  Serial.print("Ready [");

  printTimeIfAvailable();

  Serial.println("]");

  // Показывает фактический размер кольцевого буфера в отсчётах и соответствующую ему длительность звука.
  Serial.printf("Ring buffer: %u samples = %.1f sec\n", RINGBUFFER_SAMPLES, (float)RINGBUFFER_SAMPLES / SAMPLE_RATE);

  // Показывает количество отсчётов и длительность звукового фрагмента, сохраняемого перед началом записи.
  Serial.printf("Pre-buffer: %u samples = %.1f sec\n", PREBUFFER_SAMPLES, (float)PREBUFFER_SAMPLES / SAMPLE_RATE);
}

// ============================================================
// LOOP
// ============================================================

void loop() {
    if (sdCriticalError) {

    if (NextFilename >= MaxFilename) {
      digitalWrite(LED_PIN, !digitalRead(LED_PIN));
      delay(50);
      return;
    } else {

      digitalWrite(LED_PIN, !digitalRead(LED_PIN));
      delay(500);
      return;
    }
  }

  if (sdError) {

    unsigned long now = millis();

    if (sdErrorLedState) {

      if (now - sdErrorLedTime >= 1000) {

        sdErrorLedState = false;

        sdErrorLedTime = now;

        digitalWrite(LED_PIN, HIGH);
      }

    } else {

      if (now - sdErrorLedTime >= 2000) {

        sdErrorLedState = true;

        sdErrorLedTime = now;

        digitalWrite(LED_PIN, LOW
        );
      }
    }

    delay(10);

    return;
  }

  handleSoundSensor();

  delay(1);
}

Скачать:

Рассмотрим работу Arduino UNO.

Плата Arduino UNO
ISD1820 — звуковой голосовой модуль

    J1: 1 контакт (VCC) – 5V UNO; 2 (GND) – GND UNO; 4 (5 секунд) – d4 UNO; 11 (10 секунд, короткий импульс) – d2 UNO. Без перемычек.

    SP1: 1, 2 контакты через конденсаторы 2,2 µF на входы колонок.

    Анод светодиода через резистор 4,7 кОм подключен к 5V UNO, катод к d5. Светодиод начинает мигать, когда, по мнению UNO, на SD карте осталось меньше 300 MB.

    Записал голосом на ISD1820 отсчет от 21 до 30 – 10 секунд. UNO попеременно запускает воспроизведение записи на 10 и 5 секунд с интервалом 30 секунд.

Скетч для Arduino UNO:

// Arduino UNO
#include <LiquidCrystal.h>

#define PIN_TEN_SEC  2
#define PIN_FIVE_SEC 4
#define PIN_LED 5

const int rs = 8, e = 9, d4 = 10, d5 = 11, d6 = 12, d7 = 13;
LiquidCrystal lcd(rs, e, d4, d5, d6, d7);

unsigned long receivedFreeSpace = 0;
unsigned long currentFreeSpace = 0;
bool sdError = false;

unsigned long receiveFreeSpaceFromESP32() {
  String buffer = "";
  unsigned long timeout = 10000;  // Увеличиваем таймаут до 10 секунд
  unsigned long startTime = millis();
  
  while (millis() - startTime < timeout) {
    if (Serial.available()) {
      char c = Serial.read();
      
      if (c == '\n') {
        buffer.trim();
        
        if (buffer.startsWith("FREE=")) {
          String valueStr = buffer.substring(5);
          unsigned long value = valueStr.toInt();
          
          if (value > 0) {
            return value;
          }
        }
        buffer = "";
      }
      else if (c != '\r') {
        buffer += c;
      }
    }
  }
  return 0;
}

void sendOKToESP32() {
  Serial.println("OK");
  Serial.flush();
  delay(100);
  Serial.println("OK");
  Serial.flush();
  delay(100);
  Serial.println("OK");
  Serial.flush();
  delay(100);
}

void setup() {
  Serial.begin(9600);
  delay(2000);  // Увеличиваем задержку
  
  pinMode(PIN_TEN_SEC, OUTPUT);
  digitalWrite(PIN_TEN_SEC, LOW);

  pinMode(PIN_FIVE_SEC, OUTPUT);
  digitalWrite(PIN_FIVE_SEC, LOW);

  pinMode(PIN_LED, OUTPUT);
  digitalWrite(PIN_LED, HIGH);

  lcd.begin(16, 2);
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("UNO_LCD_");
  lcd.setCursor(0,1);
  lcd.print("08_03");
  delay(5000);


  bool dataReceived = false;

  while (!dataReceived) {
    // Очищаем буфер перед приёмом
    while (Serial.available()) {
      Serial.read();
    }
    
    unsigned long freeSpace = receiveFreeSpaceFromESP32();
    
    if (freeSpace > 0) {
      receivedFreeSpace = freeSpace;
      
      // Отправляем OK только один раз
      delay(500);  // Пауза перед отправкой
      sendOKToESP32();
      
      dataReceived = true;
    } else {
      delay(2000);
    }
  }
  
  currentFreeSpace = receivedFreeSpace;
}

void loop() {
  if (sdError) {
    digitalWrite(PIN_LED, !digitalRead(PIN_LED));
    delay(500);
    return;
  }
  
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print(currentFreeSpace);
  
  // Проверяем сообщения о малом месте
  if (Serial.available()) {
    String msg = Serial.readStringUntil('\n');
    if (msg.startsWith("LOW_SPACE=")) {
      lcd.clear();
      lcd.setCursor(0,0);
      lcd.print("LOW SD SPACE!");
      delay(2000);
    }
  }
  
  digitalWrite(PIN_TEN_SEC, HIGH);
  delay(10);
  digitalWrite(PIN_TEN_SEC, LOW);
  delay(30000);
  
  digitalWrite(PIN_FIVE_SEC, HIGH);
  delay(6000);
  digitalWrite(PIN_FIVE_SEC, LOW);
  delay(30000);
  
  currentFreeSpace -= 1;
  
  if (currentFreeSpace < 300) {
    sdError = true;
  }
}

Скачать:

     Купил на ОЗОНе “Цифровой аудио мини диктофон для записи голоса и разговоров + USB флешка 8 ГБ” за 1262 ₽. В принципе, ничего, с датчиком обнаружения звука. Но, после второй продолжительной записи (в течение нескольких часов) перестал распознаваться. Деньги вернули через 3 дня после возврата в пункт выдачи.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *