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 .