Учимся передавать звук с использованием протокола I2S
13 августа 2018
I2S (Inter-IC Sound) — это цифровой протокол передачи звука, который довольно часто используется в современной электронике. I2S не имеет ничего общего с I2C кроме похожего названия, поэтому важно эти протоколы не путать. В рамках поста мы постараемся разобраться, на что вообще похож I2S, и как с ним работать.
На базе устройств, использующих I2S, существует немало готовых плат, в частности:
- У Adafruit есть два микрофона — первый и второй;
- DAC на базе чипа UDA1334A можно купить у той же компании;
- Еще есть DAC с усилителем MAX98357 для вывода звука на динамик;
- На Tindie можно купить I2S-микрофон на базе ICS43434;
- На том же сайте был найден DAC на базе чипа CS4344;
При написании этого поста я использовал PmodI2S производства компании Digilent. Данный модуль построен на базе чипа CS4344 (типичная маркировка «344C 1609», даташит [PDF]). Устройство было приобретено в Чип-и-Дипе, но на момент написания этих строк оно успело куда-то пропасть с сайта магазина. Впрочем, для повторения описанных далее шагов вы можете использовать любой аналогичный модуль. Внешний вид использованного мной модуля:
Типичный I2S-сигнал выглядит в PulseView как-то так:
Здесь SCK представляет собой тактовый сигнал. WS (он же LRCLK) отвечает за выбор канала. Через SDA (он же SDIN) передаются сами данные. Сигнала MCLK, строго говоря, нет в спецификации I2S [PDF]. Но на практике многие устройства используют его для синхронизации своих внутренних операций. Обычно сюда идет тактовый сигнал с частотой в 256 раз больше частоты дискретизации звука.
Fun fact! Если хочется извлечь звук из записанного I2S-сигнала, это можно сделать такой командой:
cut -c27-30 | xxd -r -p | \
sox -t raw -B -b 16 -c 1 -e signed-integer -r 48k - audio.wav
Для экспериментов с модулем я воспользовался отладочной платой Nucleo-F411RE. Микроконтроллер, используемый в этой плате, имеет аппаратную поддержку I2S, которой и было решено воспользоваться. Какие настройки доступны в STM32CubeMX и к каким пинам микроконтроллера следует подключать модуль, вы без труда разберетесь самостоятельно по полной версии проекта. Поговорим лучше непосредственно о коде.
Генерация синусоидального сигнала с частотой 100 Гц осуществляется так:
#define TAU (2.0 * PI)
void loop() {
HAL_StatusTypeDef res;
int16_t signal[46876];
int nsamples = sizeof(signal) / sizeof(signal[0]);
int i = 0;
while(i < nsamples) {
double t = ((double)i/2.0)/((double)nsamples);
signal[i] = 32767*sin(100.0 * TAU * t); // left
signal[i+1] = signal[i]; // right
i += 2;
}
while(1) {
res = HAL_I2S_Transmit(&hi2s2, (uint16_t*)signal, nsamples,
HAL_MAX_DELAY);
if(res != HAL_OK) {
UART_Printf("I2S - ERROR, res = %d!\r\n", res);
break;
}
}
}
Интересно, что по каким-то причинам микроконтроллеры STM32 не могут использовать традиционные значения частоты дискретизации, такие, как 44100 Гц или 48000 Гц. В частности, при выборе частоты 48000 Гц реальная частота составит 46876 Гц (на 2.34% меньше). Впрочем, на слух такая разница совершенно незаметна. Все эти различия между желаемой и реальной частотой отображаются прямо в STM32CubeMX.
Подключаем осциллограф, проверяем:
Но это еще не все. Если вы попытаетесь, например, просто взять и проиграть WAV-файл с SD-карты «в лоб», то у вас ничего не получится. Звук будет периодически обрываться и слушать такое будет совершенно невозможно. Решение заключается в том, чтобы использовать прерывания и двойную буфферизацию. Другими словами, параллельно с проигрыванием одного отрывка файла должен читаться следующий отрывок. Таким образом, когда проигрывание текущего отрывка завершится, следующий отрывок будет уже готов, и не придется тратить время на его чтение с SD-карты (что и является источником обрывов в звуке).
Соответствующий код:
volatile bool read_next_chunk = false;
volatile uint16_t* signal_play_buff = NULL;
volatile uint16_t* signal_read_buff = NULL;
volatile uint16_t signal_buff1[4096];
volatile uint16_t signal_buff2[4096];
void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s) {
if(end_of_file_reached)
return;
volatile uint16_t* temp = signal_play_buff;
signal_play_buff = signal_read_buff;
signal_read_buff = temp;
int nsamples = sizeof(signal_buff1) / sizeof(signal_buff1[0]);
HAL_I2S_Transmit_IT(&hi2s2, (uint16_t*)signal_play_buff, nsamples);
read_next_chunk = true;
}
int playWavFile(const char* fname) {
UART_Printf("Openning %s...\r\n", fname);
FIL file;
FRESULT res = f_open(&file, fname, FA_READ);
if(res != FR_OK) {
UART_Printf("f_open() failed, res = %d\r\n", res);
return -1;
}
UART_Printf("File opened, reading...\r\n");
unsigned int bytesRead;
uint8_t header[44];
res = f_read(&file, header, sizeof(header), &bytesRead);
if(res != FR_OK) {
UART_Printf("f_read() failed, res = %d\r\n", res);
f_close(&file);
return -2;
}
if(memcmp((const char*)header, "RIFF", 4) != 0) {
UART_Printf("Wrong WAV signature at offset 0: "
"0x%02X 0x%02X 0x%02X 0x%02X\r\n",
header[0], header[1], header[2], header[3]);
f_close(&file);
return -3;
}
if(memcmp((const char*)header + 8, "WAVEfmt ", 8) != 0) {
UART_Printf("Wrong WAV signature at offset 8!\r\n");
f_close(&file);
return -4;
}
if(memcmp((const char*)header + 36, "data", 4) != 0) {
UART_Printf("Wrong WAV signature at offset 36!\r\n");
f_close(&file);
return -5;
}
uint32_t fileSize = 8 + (header[4] | (header[5] << 8) |
(header[6] << 16) | (header[7] << 24));
uint32_t headerSizeLeft = header[16] | (header[17] << 8) |
(header[18] << 16) | (header[19] << 24);
uint16_t compression = header[20] | (header[21] << 8);
uint16_t channelsNum = header[22] | (header[23] << 8);
uint32_t sampleRate = header[24] | (header[25] << 8) |
(header[26] << 16) | (header[27] << 24);
uint32_t bytesPerSecond = header[28] | (header[29] << 8) |
(header[30] << 16) | (header[31] << 24);
uint16_t bytesPerSample = header[32] | (header[33] << 8);
uint16_t bitsPerSamplePerChannel = header[34] | (header[35] << 8);
uint32_t dataSize = header[40] | (header[41] << 8) |
(header[42] << 16) | (header[43] << 24);
UART_Printf(
"--- WAV header ---\r\n"
"File size: %lu\r\n"
"Header size left: %lu\r\n"
"Compression (1 = no compression): %d\r\n"
"Channels num: %d\r\n"
"Sample rate: %ld\r\n"
"Bytes per second: %ld\r\n"
"Bytes per sample: %d\r\n"
"Bits per sample per channel: %d\r\n"
"Data size: %ld\r\n"
"------------------\r\n",
fileSize, headerSizeLeft, compression, channelsNum,
sampleRate, bytesPerSecond, bytesPerSample,
bitsPerSamplePerChannel, dataSize);
if(headerSizeLeft != 16) {
UART_Printf("Wrong `headerSizeLeft` value, 16 expected\r\n");
f_close(&file);
return -6;
}
if(compression != 1) {
UART_Printf("Wrong `compression` value, 1 expected\r\n");
f_close(&file);
return -7;
}
if(channelsNum != 2) {
UART_Printf("Wrong `channelsNum` value, 2 expected\r\n");
f_close(&file);
return -8;
}
if((sampleRate != 44100) || (bytesPerSample != 4) ||
(bitsPerSamplePerChannel != 16) || (bytesPerSecond != 44100*2*2)
|| (dataSize < sizeof(signal_buff1) + sizeof(signal_buff2))) {
UART_Printf("Wrong file format, 16 bit file with sample "
"rate 44100 expected\r\n");
f_close(&file);
return -9;
}
res = f_read(&file, (uint8_t*)signal_buff1, sizeof(signal_buff1),
&bytesRead);
if(res != FR_OK) {
UART_Printf("f_read() failed, res = %d\r\n", res);
f_close(&file);
return -10;
}
res = f_read(&file, (uint8_t*)signal_buff2, sizeof(signal_buff2),
&bytesRead);
if(res != FR_OK) {
UART_Printf("f_read() failed, res = %d\r\n", res);
f_close(&file);
return -11;
}
read_next_chunk = false;
end_of_file_reached = false;
signal_play_buff = signal_buff1;
signal_read_buff = signal_buff2;
HAL_StatusTypeDef hal_res;
int nsamples = sizeof(signal_buff1) / sizeof(signal_buff1[0]);
hal_res = HAL_I2S_Transmit_IT(&hi2s2, (uint16_t*)signal_buff1,
nsamples);
if(hal_res != HAL_OK) {
UART_Printf("I2S - HAL_I2S_Transmit failed, "
"hal_res = %d!\r\n", hal_res);
f_close(&file);
return -12;
}
while(dataSize >= sizeof(signal_buff1)) {
if(!read_next_chunk) {
continue;
}
read_next_chunk = false;
res = f_read(&file, (uint8_t*)signal_read_buff,
sizeof(signal_buff1), &bytesRead);
if(res != FR_OK) {
UART_Printf("f_read() failed, res = %d\r\n", res);
f_close(&file);
return -13;
}
dataSize -= sizeof(signal_buff1);
}
end_of_file_reached = true;
res = f_close(&file);
if(res != FR_OK) {
UART_Printf("f_close() failed, res = %d\r\n", res);
return -14;
}
return 0;
}
Передача данных по I2S осуществляется асинхронно при помощи процедуры HAL_I2S_Transmit_IT
. По завершении передачи данных вызывается коллбэк HAL_I2S_TxCpltCallback
. Если это известно, то остальная часть кода становится тривиальной.
Напомню, что с форматом WAV-файлов и библиотекой FatFs мы ранее познакомились в рамках статей Парсинг заголовка и проигрывание WAV-файла на Scala и Работа с FAT32 и exFAT с помощью библиотеки FatFs соответственно.
Это все, о чем я хотел рассказать. Исходники к посту вы найдете на GitHub.
Метки: STM32, Аудио, Электроника.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.