Учебное пособие по звуку I2S для ESP32
В этом руководстве вы узнаете, как использовать протокол связи I2S для передачи цифровых звуковых сигналов для записи и визуализации данных микрофона и воспроизведения музыки из внутренней памяти, а также с внешней SD-карты.
Мы также сравним разные микроконтроллеры и увидим, почему мы предпочитаем микроконтроллер ESP32 для наших проектов I2S.
Зачем нам нужен протокол I2S?
Если мы хотим воспроизвести цифровой аудиофайл с помощью платы микроконтроллера, мы должны рассмотреть всю цепочку цифрового аудио. Следующий схематический рисунок показывает, как аудиофайл сохраняется на SD-карте и считывается с платы микроконтроллера. Затем плата подключается к динамику через цифровой контакт и землю.
В моем случае у меня есть образец аудиофайла с частотой дискретизации 44,1 кГц, стереоформатом и глубиной звука 16 бит. На нашей стороне ввода, где мы хотим прочитать музыкальный файл, у нас нет проблем, потому что соединение SPI достаточно быстрое, чтобы качество не ухудшалось во время передачи.
Но со стороны выхода мы должны преобразовать цифровой сигнал в аналоговый сигнал. Это делается с помощью цифро-аналогового преобразователя (ЦАП). В зависимости от используемого микроконтроллера возникают разные проблемы:
- Arduino и ESP8266: платы Arduino, а также ESP8266 в целом не имеют внутреннего ЦАП, и поэтому вам придется создавать ЦАП с внешними компонентами.
- ESP32: ESP32 имеет внутренний ЦАП для создания аналогового выходного сигнала, однако ЦАП имеет только 8-битное разрешение. Поскольку у нас есть 16-битный входной сигнал, мы немного потеряем качество.
Но как мы можем преобразовать цифровые данные из файла WAVE в динамик? Решением этой проблемы является протокол связи I2S, который поддерживает от 4 до 32 бит данных на выборку. Чтобы сделать нашу жизнь еще проще, мы используем коммутационную плату для аудио MAX98357 I2S. Но сначала мы углубимся в протокол связи I2S.
Протокол связи I2S
В этой части руководства по I2S мы хотим подробнее рассмотреть протокол связи I2S. Поэтому мы затрагиваем три важные темы.
- 3-проводное соединение I2S
- Сетевые компоненты I2S
- Временная диаграмма I2S
В следующей таблице показано, какие платы имеют интерфейс I2S, а какие нет.
Имеет интерфейс I2S | Не имеет интерфейса I2S | |
---|---|---|
Arduino | Платы Arduino Arduino Due, Arduino MKR Zero, Arduino MKR1000 WiFi | с микроконтроллером ATmega328P, такие как Arduino Uno или Arduino Nano. |
ESP | ESP8266 ESP32 |
Из таблицы видно, что только некоторые специальные платы Arduino имеют интерфейс I2S, но не наиболее используемые платы, такие как Arduino Uno. Также все платы ESP8266 и ESP32 поддерживают интерфейс I2S, поэтому я рекомендую использовать для этого руководства плату на базе микроконтроллера ESP8266 или ESP32. В моем случае я использую микроконтроллер ESP32, потому что библиотеки, которые мы используем, поддерживают ESP32 лучше, чем ESP8266, по моему опыту.
3-проводное соединение I2S
Протокол I2S использует для связи три провода.
Последовательные часы (SCK), также называемые линией битовых часов (BCLK), используются для получения всех компонентов в одном цикле. Частота тактовых импульсов последовательного интерфейса определяется следующим образом:
Частота = Частота дискретизации * Количество бит на канал * Количество каналов.
Для моего WAVE-файла, который я использую в этом руководстве, мы уже знаем следующие переменные:
- Частота дискретизации: 44,1 кГц
- Бит на канал: 16
- Количество каналов: 2
Следовательно, тактовая частота последовательного интерфейса составляет 44,1 кГц * 16 * 2 = 1,411 МГц.
Вторая линия протокола связи I2S - это провод выбора слова (Word Select - WS) или выбора кадра (Frame Select - FS), который различает левый или правый канал.
- Если WS = 0 → используется канал 1 (левый канал)
- Если WS = 1 → используется канал 2 (правый канал)
Последний провод - это линия последовательных данных (Serial Data - SD), по которой полезная нагрузка передается с двумя дополнениями. Важно, чтобы старший бит передавался первым (сначала MSB), потому что передатчик и приемник могут иметь разную длину слова. Следовательно, ни передатчик, ни приемник не должны знать, сколько битов передается. Но что произойдет, если длина слова между передатчиком и приемником не совпадает?
- Если WS приемника > WS передатчика → слово усекается (младшие значащие биты данных устанавливаются в 0)
- Если WS приемника < WS передатчика → биты после LSB игнорируются
Сетевые компоненты I2S
Если есть несколько компонентов I2S, подключенных друг к другу, я называю это сетью I2S. Компоненты сети имеют разные имена, а также разные функции. На следующем рисунке показаны три разные сети, которые я опишу.
На первом изображении у нас есть передатчик, а также приемник. Передатчиком может быть плата ESP NodeMCU, а приемником - плата аудиоразъема I2S, которую мы описываем в следующем разделе. Также у нас есть три провода для подключения устройств I2S.
В этом первом случае передатчик является ведущим, потому что ведущий управляет последовательными линиями синхронизации (SCK) и линиями выбора слова (WS). На втором рисунке мы видим обратное, потому что получатель сообщений I2S также может быть ведущим. Следовательно, линии SCK и WS начинаются от приемника и заканчиваются на передатчике.
На третьем рисунке показано, что внешний контроллер также может быть ведущим устройством, которое генерирует SCK и WS. Контроллер подключен к узлам в сети.
Во всех сетях I2S есть только одно ведущее устройство. Может быть несколько других компонентов, которые принимают или передают звуковые данные.
Временная диаграмма I2S
Чтобы лучше понять поведение, а также функциональность протокола связи I2S, мы взглянем на следующую временную диаграмму I2S.
На временной диаграмме вы видите все три линии: SCK, WS и SD. Сначала у нас есть наши последовательные часы, которые имеют частоту дискретизации * бит на канал * количество каналов. В нашем примере 1,411 МГц. Второй канал - это строка выбора слова, которая изменяется от 1 для правого звукового канала до 0 для левого канала.
Из строки последовательных данных мы видим, что данные отправляются в каждом тактовом цикле на заднем фронте (красная пунктирная линия) → от HIGH до LOW. Для связи I2S также можно отправлять данные при изменении с НИЗКОГО на ВЫСОКИЙ.
Также мы видим, что строка WS изменяется за один такт до того, как будет передан самый старший бит (MSB). Это дает получателю время сохранить предыдущее слово и очистить входной регистр для следующего слова. MSB отправляется, когда SCK изменяется после изменения WS.
Плата MAX98357 I2S Audio Breakout
После того, как мы узнали, что можем использовать протокол связи I2S для получения звуковых данных из микроконтроллера без какого-либо снижения качества, следующая проблема заключается в том, что мы должны декодировать сигналы I2S в аналоговые сигналы, а также нам нужен усилитель для использования динамика.
- Декодер из сигнала I2S в аналоговый сигнал, потому что динамики работают только с аналоговыми сигналами.
- Усилитель увеличивает мощность аналогового сигнала для увеличения интенсивности звука.
MAX98357 - это входной усилитель с цифровой импульсно-кодовой модуляцией (PCM), который декодирует сигнал I2S в аналоговый сигнал с помощью цифроаналогового преобразователя (DAC), а также имеет встроенный усилитель. На следующем рисунке показана упрощенная блок-схема из таблицы MAX98357.
Из блок-схемы MAX98357 вы видите, что сначала сигнал I2S преобразуется в аналоговый сигнал через ЦАП, а послесловия усиливаются усилителем с заранее заданной регулировкой усиления.
Спецификация платы Audio Breakout Board MAX98357
Вы можете купить MAX98357 в качестве коммутационной платы у Adafruit или SparkFun. Продукция точно такая же. В следующей таблице показано техническое описание MAX98357.
Adafruit MAX98357A или SparkFun MAX98357A | Значение |
---|---|
Диапазон напряжения питания | 2,7… 5,5 В |
Выходная мощность | 3,2 Вт на 4 Ом при 5 В 1,8 Вт на 8 Ом при 5 В |
Выбор выходного канала | влево, вправо или влево / 2 + вправо / 2 (по умолчанию) |
Частота дискретизации | 8 кГц… 96 кГц |
Разрешение выборки | 16/32 бит |
Ток покоя | 2,4 мА |
Усилитель класса | D |
Коэффициент усиления | 3 дБ… 15 дБ (по умолчанию: 9 дБ) |
Требуются часы памяти (MCLK)? | Нет |
Рабочее напряжение MAX98357 составляет от 2,7 В до 5,5 В. Поэтому вы можете запитать микроконтроллер с платой на базе Arduino (5 В) или ESP (3,3 В). Выходная мощность составляет 3,2 Вт для динамика с сопротивлением 4 Ом и 1,8 Вт для динамика с сопротивлением 8 Ом.
Конфигурация платы по умолчанию - «моно», то есть левый и правый сигналы объединяются вместе для управления одним динамиком. Если вы хотите переключиться на стереозвук, вам нужно разрезать моно-перемычку на плате и припаять стерео соединение для левого или правого канала.
Частота дискретизации MAX98357 находится в диапазоне от 8 кГц до 96 кГц, поэтому музыка в нашем примере с частотой дискретизации 44,1 кГц идеально подходит для этой частоты дискретизации. Разрешение выборки составляет 16 или 32 бита, а ток покоя очень низкий - 2,4 мА.
Поскольку усилитель использует широтно-импульсную модуляцию для управления выходными устройствами, он относится к усилителю класса D. Коэффициент усиления усилителя составляет от 3 дБ до 15 дБ с коэффициентом усиления по умолчанию 9 дБ. В следующей таблице показано, как изменить коэффициент усиления. Ключевым моментом является то, что вывод усиления должен быть подключен к другим выводам, чтобы изменить коэффициент усиления.
Усиление | Подключение контакта GAIN |
---|---|
15 дБ | Подключен к GND через резистор 100 кОм |
12 дБ | Подключен к GND |
9 дБ | Без подключения (по умолчанию) |
6 дБ | При подключении к VDD / Vin |
3 дБ | Подключен к VDD / Vin через резистор 100 кОм |
Как записывать и визуализировать данные с помощью микрофона I2S
В первом примере мы начинаем записывать и визуализировать звуковые данные с микроконтроллера микрофона I2S SPH0645 от adafruit. В этом примере мы используем микроконтроллер ESP32 NodeMCU.
На следующем рисунке показана проводка между ESP32 NodeMCU и коммутационной платой SPH0645.
Важно подключать микроконтроллер I2S только к выводу 3,3 В. Следующий код Arduino визуализирует аналоговые звуковые данные в последовательном плоттере Arduino.
#include "driver/i2s.h" const i2s_port_t I2S_PORT = I2S_NUM_0; void setup() { Serial.begin(115200); esp_err_t err; // The I2S config as per the example const i2s_config_t i2s_config = { .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX), // Receive, not transfer .sample_rate = 16000, // 16KHz .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT, // could only get it to work with 32bits .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT, // use right channel .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // Interrupt level 1 .dma_buf_count = 4, // number of buffers .dma_buf_len = 8 // 8 samples per buffer (minimum) }; // The pin config as per the setup const i2s_pin_config_t pin_config = { .bck_io_num = 26, // Serial Clock (SCK) .ws_io_num = 25, // Word Select (WS) .data_out_num = I2S_PIN_NO_CHANGE, // not used (only for speakers) .data_in_num = 33 // Serial Data (SD) }; // Configuring the I2S driver and pins. // This function must be called before any I2S driver read/write operations. err = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL); if (err != ESP_OK) { Serial.printf("Failed installing driver: %d\n", err); while (true); } err = i2s_set_pin(I2S_PORT, &pin_config); if (err != ESP_OK) { Serial.printf("Failed setting pin: %d\n", err); while (true); } Serial.println("I2S driver installed."); } void loop() { // Read a single sample and log it for the Serial Plotter. int32_t sample = 0; int bytes_read = i2s_pop_sample(I2S_PORT, (char *)&sample, portMAX_DELAY); // no timeout if (bytes_read > 0) { Serial.println(sample); } }
В первой строке мы включаем библиотеку I2S для ESP32 и определяем используемую структуру выводов I2S, потому что только GPIO25 и GPIO26 подключены к внутреннему 8-битному ЦАП, что также показано в распиновке ESP32.
В функции настройки мы устанавливаем скорость передачи 115200, которая должна соответствовать скорости передачи в последовательном плоттере Arduino IDE, где мы отображаем аналоговые звуковые данные.
Если мы получим какую-либо ошибку во время выполнения кода, мы можем получить доступ к ошибке с помощью переменной err.
Следующим шагом в коде Arduino является определение структуры связи I2S. Устанавливаем следующие настройки:
- установите режим I2S на RX для получения данных I2S
- используйте частоту дискретизации по умолчанию 16 кГц
- установите бит на выборку равным 32, а не 16
- мы используем только правый канал микрофона
- мы используем 4 буфера, каждый длиной 8
После того, как мы установили структуру связи I2S, мы определяем контакты, которые используются на ESP32 NodeMCU для связи. В моем случае я выбираю:
- Последовательные часы (SCK) = 26
- Выбор слова (WS) = 25
- Последовательные данные (SD) = 33
- В следующем разделе настраиваются драйвер и контакты I2S. Поскольку эта часть кода глубоко погружается во внутренние функции ESP32, мы пропускаем объяснение этого раздела.
В функции цикла мы читаем аналоговый вывод из ЦАП и сохраняем данные в переменной bytes_read. Если мы получаем данные, мы выводим аналоговый звуковой сигнал на последовательный выход, чтобы визуализировать звуковую частоту в последовательном плоттере.
На следующем рисунке показан аналоговый выход последовательного монитора, когда я проигрываю музыку со своего компьютера, и микрофон слушает.
Как воспроизводить музыку из внутренней памяти ESP32
Во втором примере мы хотим воспроизводить музыку через динамик. Звуковые данные хранятся в виде массива во внутренней RAM ESP32. Мы используем плату аудиоразъема MAX98357 I2S для преобразования цифрового сигнала в аналоговый. Поэтому мы используем протокол I2S для вывода цифровых звуковых данных без потери качества.
На следующем рисунке показана проводка между ESP32 NodeMCU, коммутационной платой MAX98357 I2S и динамиком.
Для кода Arduino мы используем библиотеку ESP8266Audio от Earle F. Philhower. Чтобы включить эту библиотеку в ваш Arduino, выполните 4 шага:
- Загрузите папку github в виде zip-файла
- разархивируйте загруженную папку
- переименуйте распакованную папку в ESP8266Audio
- скопируйте папку в путь к вашей библиотеке IDE Arduino (в моем случае: C:\Users\chris\Documents\Arduino\libraries)
Мы используем следующий код Arduino из примеров библиотеки для воспроизведения музыки из внутренней памяти.
#include "AudioGeneratorAAC.h" #include "AudioOutputI2S.h" #include "AudioFileSourcePROGMEM.h" #include "sampleaac.h" AudioFileSourcePROGMEM *in; AudioGeneratorAAC *aac; AudioOutputI2S *out; void setup() { Serial.begin(115200); in = new AudioFileSourcePROGMEM(sampleaac, sizeof(sampleaac)); aac = new AudioGeneratorAAC(); out = new AudioOutputI2S(); out -> SetGain(0.125); out -> SetPinout(26, 25, 22); aac->begin(in, out); } void loop() { if (aac->isRunning()) { aac->loop(); } else { aac -> stop(); Serial.printf("Sound Generator\n"); delay(1000); } }
В первых строках мы добавляем следующие заголовочные файлы из библиотеки ESP8266Audio:
- AudioGeneratorAAC: генератор аудиовыхода с использованием декодера Helix AAC.
- AudioOutputI2S: базовый класс для интерфейсного порта I2S
- AudioFileSourcePROGMEM: сохранить «файл» как массив PROGMEM и использовать его как данные источника звука.
- sampleaac: заголовочный файл, в котором аудиофайл хранится в виде массива
Цифровые звуковые данные хранятся в заголовочном файле sampleaac. Чтобы загрузить код Arduino с файлом заголовка в EPS32, важно, чтобы Arduino (файл .ino) и заголовок (файл .h) находились в одной папке.
После того, как мы включили файлы заголовков библиотеки ESP8266Audio, мы даем для первых трех включенных файлов переменные, содержащие ссылки на классы этих файлов.
В функции настройки мы устанавливаем скорость передачи 115200 бод и инициализируем файлы заголовков. Для AudioFileSourcePROGMEM мы определяем, что образец аудиофайла находится в файле sampleaac с размером содержащего его массива.
Объект AudioOutputI2S имеет разные функции. Мы используем функцию SetGain, чтобы уменьшить громкость динамика, и определяем распиновку с помощью функции SetPinout. В моем случае я выбираю следующую распиновку по умолчанию:
- Последовательные часы (SCK) = 26
- Выбор слова (WS) = 25
- Последовательные данные (SD) = 22
Но не стесняйтесь выбирать другие цифровые выводы микроконтроллера EPS32.
Последним шагом функции настройки является подключение входных звуковых данных из внутренней памяти программы к аудиовыходу I2S с помощью AudioGeneratorAAC.
В функции цикла звуковой генератор продолжает работать до тех пор, пока весь звуковой массив не пройдет через генератор. Когда генератор завершит работу, он сообщит об этом на последовательный выход.
Как воспроизвести файл WAVE на ESP32 с внешней SD-карты
В нашем последнем проекте мы хотим воспроизвести WAVE-файл, который я упомянул в начале этого руководства, через ESP32 NodeMCU и динамик. Поскольку ESP32 должен читать файл WAVE и пересылать цифровой аудиосигнал на MAX98357A, мы должны использовать SD-карту с файлом WAVE на ней. Вы также можете использовать файл MP3 вместо файла WAVE.
На следующем рисунке показано подключение ESP32 NodeMCU к модулю карты (Micro) SD, MAX98357A и динамику. На картинке вы видите, что вам нужно изменить контакт DIN MAX98357A, по сравнению со вторым проектом.
Прежде чем мы погрузимся в код Arduino, мы должны подготовить (Micro) SD-карту. Файловая система должна быть FAT16 или FAT32. В зависимости от модуля SD-карты для SD-карты существует ограничение в 32 ГБ. Я использую карту micro SD объемом 32 ГБ, отформатированную как FAT32, и копирую файл WAVE без папки на SD-карту.
Для этого проекта мы используем библиотеку ESP32-audioI2S Arduino от schreibfaul1. Вы можете скачать библиотеку в виде zip-файла с её страницы на GitHub. Поскольку библиотека имеет имя audio, а в Arduino уже существует библиотека с таким же именем, мы включаем библиотеку через IDE Arduino:
- Откройте IDE Arduino.
- Перейдите к (см. Следующий рисунок): Скетч → Подключить библиотеку → Добавить .ZIP библиотеку...
- Выберите загруженную библиотеку
Скрипт Arduino основан на примере скрипта schreibfaul1, но я сократил скрипт оставив только части, необходимые для воспроизведения файла WAVE, и удалил все части для потоковой передачи WiFi.
#include "Audio.h" #include "SD.h" #include "FS.h" // Digital I/O used #define SD_CS 5 #define SPI_MOSI 23 #define SPI_MISO 19 #define SPI_SCK 18 #define I2S_DOUT 25 #define I2S_BCLK 27 #define I2S_LRC 26 Audio audio; void setup() { pinMode(SD_CS, OUTPUT); digitalWrite(SD_CS, HIGH); SPI.begin(SPI_SCK, SPI_MISO, SPI_MOSI); Serial.begin(115200); SD.begin(SD_CS); audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT); audio.setVolume(10); // 0...21 audio.connecttoFS(SD, "Ensoniq-ZR-76-01-Dope-77.wav"); } void loop() { audio.loop(); } // optional void audio_info(const char *info) { Serial.print("info "); Serial.println(info); } void audio_id3data(const char *info) { //id3 metadata Serial.print("id3data "); Serial.println(info); } void audio_eof_mp3(const char *info) { //end of file Serial.print("eof_mp3 "); Serial.println(info); } void audio_showstation(const char *info) { Serial.print("station "); Serial.println(info); } void audio_showstreaminfo(const char *info) { Serial.print("streaminfo "); Serial.println(info); } void audio_showstreamtitle(const char *info) { Serial.print("streamtitle "); Serial.println(info); } void audio_bitrate(const char *info) { Serial.print("bitrate "); Serial.println(info); } void audio_commercial(const char *info) { //duration in sec Serial.print("commercial "); Serial.println(info); } void audio_icyurl(const char *info) { //homepage Serial.print("icyurl "); Serial.println(info); } void audio_lasthost(const char *info) { //stream URL played Serial.print("lasthost "); Serial.println(info); } void audio_eof_speech(const char *info) { Serial.print("eof_speech "); Serial.println(info); }
В первой части сценария Arduino для ESP32 мы включаем все библиотеки и определяем контакты, которые используются для подключения ESP32 NodeMCU к MAX98357A и модулю SD-карты.
После инициализации объекта Audio с именем «audio» вызывается функция настройки. В функции настройки определяются контакты и SPI-соединение для связи с SD-картой. Скорость передачи установлена 115200, и объект SD-карты также инициализируется.
Для объекта audio устанавливаем распиновку, и уменьшаем громкость звука до 10. Вы можете регулировать громкость звука от 0 до 21. Последняя часть функции настройки - это подключение входов и выходов в этом примере. Поэтому мы связываем аудиообъект с объектом SD-карты и определяем путь к WAVE-файлу. Если вы помещаете звуковой файл в папку, вам необходимо скопировать весь путь к звуковому файлу с косой чертой («/»).
В функции цикла нам нужно только перебрать предварительно сконфигурированный аудио объект для воспроизведения музыки.
Последняя часть интересна, если вы хотите распечатать некоторые детали звукового файла на последовательном мониторе. На следующем рисунке показан последовательный вывод в моем примере. В первом разделе содержится информация о загрузке ESP32, которая отображается на последовательном мониторе, если скорость передачи установлена 115200.
В первой части этой статьи я вычислил частоту для тактовой частоты последовательного порта 44,1 кГц * 16 * 2 = 1,411 МГц. Теперь я хочу доказать, что частота I2S-соединения (последовательных часов SCK) между ESP32 и MAX98357A составляет 1,411 МГц. Поэтому я подключил линию CLK к своему USB-осциллографу и добавил измерение частоты.
На следующем рисунке видно, что мои расчеты верны и частота составляет 1,411 МГц.