Для тех, кто заходил сюда до 09.10.2026, сообщаю об ошибке в скетче.
Как ни странно, даже при длительном тестировании я не заметил, что дата и время записи файлов на SD-карте не меняются. Просто не обращал на это внимания — смотрел только в монитор последовательного порта, где всё было ОК.
Скетч ESP32_VOS_INMP441_SD_Test_02.ino исправлен, как и листинг на странице.
Добавлю каплю мёда. Вам не нужно каждый раз зажимать кнопку BOOT (P0) и нажимать RST (EN), чтобы загрузить скетч. Схема платы спроектирована так, что чип CH343 сам управляет линиями P0 и EN через сигналы DTR и RTS, поступающие от компьютера.
Эта статья является развитием следующего проекта для 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-гигабайтной можно почитать здесь:
Установлено, что с возрастанием количества записанных файлов на карту увеличивается время “SD OPEN”. Перенос файлов в каталог не устраняет проблему, только немного смягчает. Замена #include «FS.h» и #include «SD.h» на #include «SdFat.h» сократила время процентов на 30. Параметр #define SD_SPI_SPEED SD_SCK_MHZ(16) (16 МГц) сработал с картой на 16 ГБ, на 20 МГц – карта ушла в отказ. Свой окончательный вариант для автономной работы выложу, когда соберу устройство на аккумуляторе, а вы сейчас можете проанализировать временные характеристики по логам и модернизировать скетч согласно вашим предпочтениям.
Сначала я реализовал включение записи по опросу, но затем перешел на обработку по прерыванию. По ходу тестирования выявилось много проблем, видимо, не связанных с методом регистрации звука, поэтому сейчас я не уверен, что режим опроса хуже режима по прерыванию, но проверять уже не буду.
Таблица подключения модулей к ESP32-WROOM (в Arduino IDE выбрал ESP32 Dev Module, обозначения контактов ESP32 относятся к плате расширения):
КНОПКИ
SD
ESP32
INMP441
VOS
UART_UNO
LED_PINS
RST
EN
BOOT
P0
GND
GND
VCC
5V
MISO
P19
MOSI
P23
SCK
P18
CS(SS)
P5
P14
WS
P32
SCK
P33
SD
GND
L/R
3,3V
VDD
GND
GND
3,3V
VCC
GND
GND
P4
OUT
P26
UNO_TX
P25
UNO_RX
P2
KATHODE
3,3V
4,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);
}
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 дня после возврата в пункт выдачи.